All Files (55.62% covered at 23.96 hits/line)
473 files in total.
21995 relevant lines.
12233 lines covered and
9762 lines missed
-
2
require 'mail'
-
2
require 'action_mailer/collector'
-
2
require 'active_support/core_ext/string/inflections'
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/module/anonymous'
-
-
2
require 'action_mailer/log_subscriber'
-
-
2
module ActionMailer
-
# Action Mailer allows you to send email from your application using a mailer model and views.
-
#
-
# = Mailer Models
-
#
-
# To use Action Mailer, you need to create a mailer model.
-
#
-
# $ rails generate mailer Notifier
-
#
-
# The generated model inherits from <tt>ApplicationMailer</tt> which in turn
-
# inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods
-
# used to generate an email message. In these methods, you can setup variables to be used in
-
# the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments.
-
#
-
# class ApplicationMailer < ActionMailer::Base
-
# default from: 'from@exmaple.com'
-
# layout 'mailer'
-
# end
-
#
-
# class Notifier < ApplicationMailer
-
# default from: 'no-reply@example.com',
-
# return_path: 'system@example.com'
-
#
-
# def welcome(recipient)
-
# @account = recipient
-
# mail(to: recipient.email_address_with_name,
-
# bcc: ["bcc@example.com", "Order Watcher <watcher@example.com>"])
-
# end
-
# end
-
#
-
# Within the mailer method, you have access to the following methods:
-
#
-
# * <tt>attachments[]=</tt> - Allows you to add attachments to your email in an intuitive
-
# manner; <tt>attachments['filename.png'] = File.read('path/to/filename.png')</tt>
-
#
-
# * <tt>attachments.inline[]=</tt> - Allows you to add an inline attachment to your email
-
# in the same manner as <tt>attachments[]=</tt>
-
#
-
# * <tt>headers[]=</tt> - Allows you to specify any header field in your email such
-
# as <tt>headers['X-No-Spam'] = 'True'</tt>. Note that declaring a header multiple times
-
# will add many fields of the same name. Read #headers doc for more information.
-
#
-
# * <tt>headers(hash)</tt> - Allows you to specify multiple headers in your email such
-
# as <tt>headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})</tt>
-
#
-
# * <tt>mail</tt> - Allows you to specify email to be sent.
-
#
-
# The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt>
-
# will accept (any valid email header including optional fields).
-
#
-
# The mail method, if not passed a block, will inspect your views and send all the views with
-
# the same name as the method, so the above action would send the +welcome.text.erb+ view
-
# file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email.
-
#
-
# If you want to explicitly render only certain templates, pass a block:
-
#
-
# mail(to: user.email) do |format|
-
# format.text
-
# format.html
-
# end
-
#
-
# The block syntax is also useful in providing information specific to a part:
-
#
-
# mail(to: user.email) do |format|
-
# format.text(content_transfer_encoding: "base64")
-
# format.html
-
# end
-
#
-
# Or even to render a special view:
-
#
-
# mail(to: user.email) do |format|
-
# format.text
-
# format.html { render "some_other_template" }
-
# end
-
#
-
# = Mailer views
-
#
-
# Like Action Controller, each mailer class has a corresponding view directory in which each
-
# method of the class looks for a template with its name.
-
#
-
# To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same
-
# name as the method in your mailer model. For example, in the mailer defined above, the template at
-
# <tt>app/views/notifier/welcome.text.erb</tt> would be used to generate the email.
-
#
-
# Variables defined in the methods of your mailer model are accessible as instance variables in their
-
# corresponding view.
-
#
-
# Emails by default are sent in plain text, so a sample view for our model example might look like this:
-
#
-
# Hi <%= @account.name %>,
-
# Thanks for joining our service! Please check back often.
-
#
-
# You can even use Action View helpers in these views. For example:
-
#
-
# You got a new note!
-
# <%= truncate(@note.body, length: 25) %>
-
#
-
# If you need to access the subject, from or the recipients in the view, you can do that through message object:
-
#
-
# You got a new note from <%= message.from %>!
-
# <%= truncate(@note.body, length: 25) %>
-
#
-
#
-
# = Generating URLs
-
#
-
# URLs can be generated in mailer views using <tt>url_for</tt> or named routes. Unlike controllers from
-
# Action Pack, the mailer instance doesn't have any context about the incoming request, so you'll need
-
# to provide all of the details needed to generate a URL.
-
#
-
# When using <tt>url_for</tt> you'll need to provide the <tt>:host</tt>, <tt>:controller</tt>, and <tt>:action</tt>:
-
#
-
# <%= url_for(host: "example.com", controller: "welcome", action: "greeting") %>
-
#
-
# When using named routes you only need to supply the <tt>:host</tt>:
-
#
-
# <%= users_url(host: "example.com") %>
-
#
-
# You should use the <tt>named_route_url</tt> style (which generates absolute URLs) and avoid using the
-
# <tt>named_route_path</tt> style (which generates relative URLs), since clients reading the mail will
-
# have no concept of a current URL from which to determine a relative path.
-
#
-
# It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt>
-
# option as a configuration option in <tt>config/application.rb</tt>:
-
#
-
# config.action_mailer.default_url_options = { host: "example.com" }
-
#
-
# When you decide to set a default <tt>:host</tt> for your mailers, then you need to make sure to use the
-
# <tt>only_path: false</tt> option when using <tt>url_for</tt>. Since the <tt>url_for</tt> view helper
-
# will generate relative URLs by default when a <tt>:host</tt> option isn't explicitly provided, passing
-
# <tt>only_path: false</tt> will ensure that absolute URLs are generated.
-
#
-
# = Sending mail
-
#
-
# Once a mailer action and template are defined, you can deliver your message or create it and save it
-
# for delivery later:
-
#
-
# Notifier.welcome(User.first).deliver_now # sends the email
-
# mail = Notifier.welcome(User.first) # => an ActionMailer::MessageDelivery object
-
# mail.deliver_now # sends the email
-
#
-
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If
-
# you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on
-
# the <tt>ActionMailer::MessageDelivery</tt> object.
-
#
-
# Notifier.welcome(User.first).message # => a Mail::Message object
-
#
-
# Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside
-
# of the request-response cycle, so the user doesn't have to wait on it):
-
#
-
# Notifier.welcome(User.first).deliver_later # enqueue the email sending to Active Job
-
#
-
# You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
-
#
-
# = Multipart Emails
-
#
-
# Multipart messages can also be used implicitly because Action Mailer will automatically detect and use
-
# multipart templates, where each template is named after the name of the action, followed by the content
-
# type. Each such detected template will be added as a separate part to the message.
-
#
-
# For example, if the following templates exist:
-
# * signup_notification.text.erb
-
# * signup_notification.html.erb
-
# * signup_notification.xml.builder
-
# * signup_notification.yml.erb
-
#
-
# Each would be rendered and added as a separate part to the message, with the corresponding content
-
# type. The content type for the entire message is automatically set to <tt>multipart/alternative</tt>,
-
# which indicates that the email contains multiple different representations of the same email
-
# body. The same instance variables defined in the action are passed to all email templates.
-
#
-
# Implicit template rendering is not performed if any attachments or parts have been added to the email.
-
# This means that you'll have to manually add each part to the email and set the content type of the email
-
# to <tt>multipart/alternative</tt>.
-
#
-
# = Attachments
-
#
-
# Sending attachment in emails is easy:
-
#
-
# class Notifier < ApplicationMailer
-
# def welcome(recipient)
-
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
-
# mail(to: recipient, subject: "New account information")
-
# end
-
# end
-
#
-
# Which will (if it had both a <tt>welcome.text.erb</tt> and <tt>welcome.html.erb</tt>
-
# template in the view directory), send a complete <tt>multipart/mixed</tt> email with two parts,
-
# the first part being a <tt>multipart/alternative</tt> with the text and HTML email parts inside,
-
# and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book
-
# with the filename +free_book.pdf+.
-
#
-
# If you need to send attachments with no content, you need to create an empty view for it,
-
# or add an empty body parameter like this:
-
#
-
# class Notifier < ApplicationMailer
-
# def welcome(recipient)
-
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
-
# mail(to: recipient, subject: "New account information", body: "")
-
# end
-
# end
-
#
-
# = Inline Attachments
-
#
-
# You can also specify that a file should be displayed inline with other HTML. This is useful
-
# if you want to display a corporate logo or a photo.
-
#
-
# class Notifier < ApplicationMailer
-
# def welcome(recipient)
-
# attachments.inline['photo.png'] = File.read('path/to/photo.png')
-
# mail(to: recipient, subject: "Here is what we look like")
-
# end
-
# end
-
#
-
# And then to reference the image in the view, you create a <tt>welcome.html.erb</tt> file and
-
# make a call to +image_tag+ passing in the attachment you want to display and then call
-
# +url+ on the attachment to get the relative content id path for the image source:
-
#
-
# <h1>Please Don't Cringe</h1>
-
#
-
# <%= image_tag attachments['photo.png'].url -%>
-
#
-
# As we are using Action View's +image_tag+ method, you can pass in any other options you want:
-
#
-
# <h1>Please Don't Cringe</h1>
-
#
-
# <%= image_tag attachments['photo.png'].url, alt: 'Our Photo', class: 'photo' -%>
-
#
-
# = Observing and Intercepting Mails
-
#
-
# Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to
-
# register classes that are called during the mail delivery life cycle.
-
#
-
# An observer class must implement the <tt>:delivered_email(message)</tt> method which will be
-
# called once for every email sent after the email has been sent.
-
#
-
# An interceptor class must implement the <tt>:delivering_email(message)</tt> method which will be
-
# called before the email is sent, allowing you to make modifications to the email before it hits
-
# the delivery agents. Your class should make any needed modifications directly to the passed
-
# in <tt>Mail::Message</tt> instance.
-
#
-
# = Default Hash
-
#
-
# Action Mailer provides some intelligent defaults for your emails, these are usually specified in a
-
# default method inside the class definition:
-
#
-
# class Notifier < ApplicationMailer
-
# default sender: 'system@example.com'
-
# end
-
#
-
# You can pass in any header value that a <tt>Mail::Message</tt> accepts. Out of the box,
-
# <tt>ActionMailer::Base</tt> sets the following:
-
#
-
# * <tt>mime_version: "1.0"</tt>
-
# * <tt>charset: "UTF-8",</tt>
-
# * <tt>content_type: "text/plain",</tt>
-
# * <tt>parts_order: [ "text/plain", "text/enriched", "text/html" ]</tt>
-
#
-
# <tt>parts_order</tt> and <tt>charset</tt> are not actually valid <tt>Mail::Message</tt> header fields,
-
# but Action Mailer translates them appropriately and sets the correct values.
-
#
-
# As you can pass in any header, you need to either quote the header as a string, or pass it in as
-
# an underscored symbol, so the following will work:
-
#
-
# class Notifier < ApplicationMailer
-
# default 'Content-Transfer-Encoding' => '7bit',
-
# content_description: 'This is a description'
-
# end
-
#
-
# Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you
-
# can define methods that evaluate as the message is being generated:
-
#
-
# class Notifier < ApplicationMailer
-
# default 'X-Special-Header' => Proc.new { my_method }
-
#
-
# private
-
#
-
# def my_method
-
# 'some complex call'
-
# end
-
# end
-
#
-
# Note that the proc is evaluated right at the start of the mail message generation, so if you
-
# set something in the defaults using a proc, and then set the same thing inside of your
-
# mailer method, it will get over written by the mailer method.
-
#
-
# It is also possible to set these default options that will be used in all mailers through
-
# the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>:
-
#
-
# config.action_mailer.default_options = { from: "no-reply@example.org" }
-
#
-
# = Callbacks
-
#
-
# You can specify callbacks using before_action and after_action for configuring your messages.
-
# This may be useful, for example, when you want to add default inline attachments for all
-
# messages sent out by a certain mailer class:
-
#
-
# class Notifier < ApplicationMailer
-
# before_action :add_inline_attachment!
-
#
-
# def welcome
-
# mail
-
# end
-
#
-
# private
-
#
-
# def add_inline_attachment!
-
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
-
# end
-
# end
-
#
-
# Callbacks in Action Mailer are implemented using
-
# <tt>AbstractController::Callbacks</tt>, so you can define and configure
-
# callbacks in the same manner that you would use callbacks in classes that
-
# inherit from <tt>ActionController::Base</tt>.
-
#
-
# Note that unless you have a specific reason to do so, you should prefer using before_action
-
# rather than after_action in your Action Mailer classes so that headers are parsed properly.
-
#
-
# = Previewing emails
-
#
-
# You can preview your email templates visually by adding a mailer preview file to the
-
# <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
-
# with database data, you'll need to write some scenarios to load messages with fake data:
-
#
-
# class NotifierPreview < ActionMailer::Preview
-
# def welcome
-
# Notifier.welcome(User.first)
-
# end
-
# end
-
#
-
# Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer
-
# method without the additional <tt>deliver_now</tt> / <tt>deliver_later</tt>. The location of the
-
# mailer previews directory can be configured using the <tt>preview_path</tt> option which has a default
-
# of <tt>test/mailers/previews</tt>:
-
#
-
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
-
#
-
# An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt>
-
# on a running development server instance.
-
#
-
# Previews can also be intercepted in a similar manner as deliveries can be by registering
-
# a preview interceptor that has a <tt>previewing_email</tt> method:
-
#
-
# class CssInlineStyler
-
# def self.previewing_email(message)
-
# # inline CSS styles
-
# end
-
# end
-
#
-
# config.action_mailer.preview_interceptors :css_inline_styler
-
#
-
# Note that interceptors need to be registered both with <tt>register_interceptor</tt>
-
# and <tt>register_preview_interceptor</tt> if they should operate on both sending and
-
# previewing emails.
-
#
-
# = Configuration options
-
#
-
# These options are specified on the class level, like
-
# <tt>ActionMailer::Base.raise_delivery_errors = true</tt>
-
#
-
# * <tt>default_options</tt> - You can pass this in at a class level as well as within the class itself as
-
# per the above section.
-
#
-
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
-
# Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
-
#
-
# * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
-
# * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default
-
# "localhost" setting.
-
# * <tt>:port</tt> - On the off chance that your mail server doesn't run on port 25, you can change it.
-
# * <tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.
-
# * <tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.
-
# * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
-
# * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the
-
# authentication type here.
-
# This is a symbol and one of <tt>:plain</tt> (will send the password Base64 encoded), <tt>:login</tt> (will
-
# send the password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange
-
# information and a cryptographic Message Digest 5 algorithm to hash important information)
-
# * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts
-
# to use it. Defaults to <tt>true</tt>.
-
# * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
-
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
-
# of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>,
-
# <tt>'fail_if_no_peer_cert'</tt>) or directly the constant (<tt>OpenSSL::SSL::VERIFY_NONE</tt>,
-
# <tt>OpenSSL::SSL::VERIFY_PEER</tt>, ...).
-
#
-
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
-
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
-
# * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i -t</tt> with <tt>-f sender@address</tt>
-
# added automatically before the message is sent.
-
#
-
# * <tt>file_settings</tt> - Allows you to override options for the <tt>:file</tt> delivery method.
-
# * <tt>:location</tt> - The directory into which emails will be written. Defaults to the application
-
# <tt>tmp/mails</tt>.
-
#
-
# * <tt>raise_delivery_errors</tt> - Whether or not errors should be raised if the email fails to be delivered.
-
#
-
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default),
-
# <tt>:sendmail</tt>, <tt>:test</tt>, and <tt>:file</tt>. Or you may provide a custom delivery method
-
# object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to
-
# implement for a custom delivery agent.
-
#
-
# * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you
-
# call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can
-
# be turned off to aid in functional testing.
-
#
-
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
-
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
-
2
class Base < AbstractController::Base
-
2
include DeliveryMethods
-
2
include Previews
-
-
2
abstract!
-
-
2
include AbstractController::Rendering
-
-
2
include AbstractController::Logger
-
2
include AbstractController::Helpers
-
2
include AbstractController::Translation
-
2
include AbstractController::AssetPaths
-
2
include AbstractController::Callbacks
-
-
2
include ActionView::Layouts
-
-
2
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
-
-
2
def _protected_ivars # :nodoc:
-
PROTECTED_IVARS
-
end
-
-
2
helper ActionMailer::MailHelper
-
-
2
private_class_method :new #:nodoc:
-
-
2
class_attribute :default_params
-
2
self.default_params = {
-
mime_version: "1.0",
-
charset: "UTF-8",
-
content_type: "text/plain",
-
parts_order: [ "text/plain", "text/enriched", "text/html" ]
-
}.freeze
-
-
2
class << self
-
# Register one or more Observers which will be notified when mail is delivered.
-
2
def register_observers(*observers)
-
2
observers.flatten.compact.each { |observer| register_observer(observer) }
-
end
-
-
# Register one or more Interceptors which will be called before mail is sent.
-
2
def register_interceptors(*interceptors)
-
2
interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
-
end
-
-
# Register an Observer which will be notified when mail is delivered.
-
# Either a class, string or symbol can be passed in as the Observer.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
2
def register_observer(observer)
-
delivery_observer = case observer
-
when String, Symbol
-
observer.to_s.camelize.constantize
-
else
-
observer
-
end
-
-
Mail.register_observer(delivery_observer)
-
end
-
-
# Register an Interceptor which will be called before mail is sent.
-
# Either a class, string or symbol can be passed in as the Interceptor.
-
# If a string or symbol is passed in it will be camelized and constantized.
-
2
def register_interceptor(interceptor)
-
delivery_interceptor = case interceptor
-
when String, Symbol
-
interceptor.to_s.camelize.constantize
-
else
-
interceptor
-
end
-
-
Mail.register_interceptor(delivery_interceptor)
-
end
-
-
# Returns the name of current mailer. This method is also being used as a path for a view lookup.
-
# If this is an anonymous mailer, this method will return +anonymous+ instead.
-
2
def mailer_name
-
@mailer_name ||= anonymous? ? "anonymous" : name.underscore
-
end
-
# Allows to set the name of current mailer.
-
2
attr_writer :mailer_name
-
2
alias :controller_path :mailer_name
-
-
# Sets the defaults through app configuration:
-
#
-
# config.action_mailer.default(from: "no-reply@example.org")
-
#
-
# Aliased by ::default_options=
-
2
def default(value = nil)
-
self.default_params = default_params.merge(value).freeze if value
-
default_params
-
end
-
# Allows to set defaults through app configuration:
-
#
-
# config.action_mailer.default_options = { from: "no-reply@example.org" }
-
2
alias :default_options= :default
-
-
# Receives a raw email, parses it into an email object, decodes it,
-
# instantiates a new mailer, and passes the email object to the mailer
-
# object's +receive+ method.
-
#
-
# If you want your mailer to be able to process incoming messages, you'll
-
# need to implement a +receive+ method that accepts the raw email string
-
# as a parameter:
-
#
-
# class MyMailer < ActionMailer::Base
-
# def receive(mail)
-
# # ...
-
# end
-
# end
-
2
def receive(raw_mail)
-
ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
-
mail = Mail.new(raw_mail)
-
set_payload_for_mail(payload, mail)
-
new.receive(mail)
-
end
-
end
-
-
# Wraps an email delivery inside of <tt>ActiveSupport::Notifications</tt> instrumentation.
-
#
-
# This method is actually called by the <tt>Mail::Message</tt> object itself
-
# through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
-
# calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
-
# nothing except tell the logger you sent the email.
-
2
def deliver_mail(mail) #:nodoc:
-
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
-
set_payload_for_mail(payload, mail)
-
yield # Let Mail do the delivery actions
-
end
-
end
-
-
2
def respond_to?(method, include_private = false) #:nodoc:
-
6
super || action_methods.include?(method.to_s)
-
end
-
-
2
protected
-
-
2
def set_payload_for_mail(payload, mail) #:nodoc:
-
payload[:mailer] = name
-
payload[:message_id] = mail.message_id
-
payload[:subject] = mail.subject
-
payload[:to] = mail.to
-
payload[:from] = mail.from
-
payload[:bcc] = mail.bcc if mail.bcc.present?
-
payload[:cc] = mail.cc if mail.cc.present?
-
payload[:date] = mail.date
-
payload[:mail] = mail.encoded
-
end
-
-
2
def method_missing(method_name, *args) # :nodoc:
-
if action_methods.include?(method_name.to_s)
-
MessageDelivery.new(self, method_name, *args)
-
else
-
super
-
end
-
end
-
end
-
-
2
attr_internal :message
-
-
# Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
-
# will be initialized according to the named method. If not, the mailer will
-
# remain uninitialized (useful when you only need to invoke the "receive"
-
# method, for instance).
-
2
def initialize(method_name=nil, *args)
-
super()
-
@_mail_was_called = false
-
@_message = Mail.new
-
process(method_name, *args) if method_name
-
end
-
-
2
def process(method_name, *args) #:nodoc:
-
payload = {
-
mailer: self.class.name,
-
action: method_name
-
}
-
-
ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
-
lookup_context.skip_default_locale!
-
-
super
-
@_message = NullMail.new unless @_mail_was_called
-
end
-
end
-
-
2
class NullMail #:nodoc:
-
2
def body; '' end
-
2
def header; {} end
-
-
2
def respond_to?(string, include_all=false)
-
true
-
end
-
-
2
def method_missing(*args)
-
nil
-
end
-
end
-
-
# Returns the name of the mailer object.
-
2
def mailer_name
-
self.class.mailer_name
-
end
-
-
# Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt>
-
# object which will add them to itself.
-
#
-
# headers['X-Special-Domain-Specific-Header'] = "SecretValue"
-
#
-
# You can also pass a hash into headers of header field names and values,
-
# which will then be set on the <tt>Mail::Message</tt> object:
-
#
-
# headers 'X-Special-Domain-Specific-Header' => "SecretValue",
-
# 'In-Reply-To' => incoming.message_id
-
#
-
# The resulting <tt>Mail::Message</tt> will have the following in its header:
-
#
-
# X-Special-Domain-Specific-Header: SecretValue
-
#
-
# Note about replacing already defined headers:
-
#
-
# * +subject+
-
# * +sender+
-
# * +from+
-
# * +to+
-
# * +cc+
-
# * +bcc+
-
# * +reply-to+
-
# * +orig-date+
-
# * +message-id+
-
# * +references+
-
#
-
# Fields can only appear once in email headers while other fields such as
-
# <tt>X-Anything</tt> can appear multiple times.
-
#
-
# If you want to replace any header which already exists, first set it to
-
# +nil+ in order to reset the value otherwise another field will be added
-
# for the same header.
-
2
def headers(args = nil)
-
if args
-
@_message.headers(args)
-
else
-
@_message
-
end
-
end
-
-
# Allows you to add attachments to an email, like so:
-
#
-
# mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
-
#
-
# If you do this, then Mail will take the file name and work out the mime type
-
# set the Content-Type, Content-Disposition, Content-Transfer-Encoding and
-
# base64 encode the contents of the attachment all for you.
-
#
-
# You can also specify overrides if you want by passing a hash instead of a string:
-
#
-
# mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
-
# content: File.read('/path/to/filename.jpg')}
-
#
-
# If you want to use a different encoding than Base64, you can pass an encoding in,
-
# but then it is up to you to pass in the content pre-encoded, and don't expect
-
# Mail to know how to decode this data:
-
#
-
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
-
# mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
-
# encoding: 'SpecialEncoding',
-
# content: file_content }
-
#
-
# You can also search for specific attachments:
-
#
-
# # By Filename
-
# mail.attachments['filename.jpg'] # => Mail::Part object or nil
-
#
-
# # or by index
-
# mail.attachments[0] # => Mail::Part (first attachment)
-
#
-
2
def attachments
-
if @_mail_was_called
-
LateAttachmentsProxy.new(@_message.attachments)
-
else
-
@_message.attachments
-
end
-
end
-
-
2
class LateAttachmentsProxy < SimpleDelegator
-
2
def inline; _raise_error end
-
2
def []=(_name, _content); _raise_error end
-
-
2
private
-
2
def _raise_error
-
raise RuntimeError, "Can't add attachments after `mail` was called.\n" \
-
"Make sure to use `attachments[]=` before calling `mail`."
-
end
-
end
-
-
# The main method that creates the message and renders the email templates. There are
-
# two ways to call this method, with a block, or without a block.
-
#
-
# It accepts a headers hash. This hash allows you to specify
-
# the most used headers in an email message, these are:
-
#
-
# * +:subject+ - The subject of the message, if this is omitted, Action Mailer will
-
# ask the Rails I18n class for a translated +:subject+ in the scope of
-
# <tt>[mailer_scope, action_name]</tt> or if this is missing, will translate the
-
# humanized version of the +action_name+
-
# * +:to+ - Who the message is destined for, can be a string of addresses, or an array
-
# of addresses.
-
# * +:from+ - Who the message is from
-
# * +:cc+ - Who you would like to Carbon-Copy on this email, can be a string of addresses,
-
# or an array of addresses.
-
# * +:bcc+ - Who you would like to Blind-Carbon-Copy on this email, can be a string of
-
# addresses, or an array of addresses.
-
# * +:reply_to+ - Who to set the Reply-To header of the email to.
-
# * +:date+ - The date to say the email was sent on.
-
#
-
# You can set default values for any of the above headers (except +:date+)
-
# by using the ::default class method:
-
#
-
# class Notifier < ActionMailer::Base
-
# default from: 'no-reply@test.lindsaar.net',
-
# bcc: 'email_logger@test.lindsaar.net',
-
# reply_to: 'bounces@test.lindsaar.net'
-
# end
-
#
-
# If you need other headers not listed above, you can either pass them in
-
# as part of the headers hash or use the <tt>headers['name'] = value</tt>
-
# method.
-
#
-
# When a +:return_path+ is specified as header, that value will be used as
-
# the 'envelope from' address for the Mail message. Setting this is useful
-
# when you want delivery notifications sent to a different address than the
-
# one in +:from+. Mail will actually use the +:return_path+ in preference
-
# to the +:sender+ in preference to the +:from+ field for the 'envelope
-
# from' value.
-
#
-
# If you do not pass a block to the +mail+ method, it will find all
-
# templates in the view paths using by default the mailer name and the
-
# method name that it is being called from, it will then create parts for
-
# each of these templates intelligently, making educated guesses on correct
-
# content type and sequence, and return a fully prepared <tt>Mail::Message</tt>
-
# ready to call <tt>:deliver</tt> on to send.
-
#
-
# For example:
-
#
-
# class Notifier < ActionMailer::Base
-
# default from: 'no-reply@test.lindsaar.net'
-
#
-
# def welcome
-
# mail(to: 'mikel@test.lindsaar.net')
-
# end
-
# end
-
#
-
# Will look for all templates at "app/views/notifier" with name "welcome".
-
# If no welcome template exists, it will raise an ActionView::MissingTemplate error.
-
#
-
# However, those can be customized:
-
#
-
# mail(template_path: 'notifications', template_name: 'another')
-
#
-
# And now it will look for all templates at "app/views/notifications" with name "another".
-
#
-
# If you do pass a block, you can render specific templates of your choice:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text
-
# format.html
-
# end
-
#
-
# You can even render plain text directly without using a template:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text { render plain: "Hello Mikel!" }
-
# format.html { render html: "<h1>Hello Mikel!</h1>".html_safe }
-
# end
-
#
-
# Which will render a +multipart/alternative+ email with +text/plain+ and
-
# +text/html+ parts.
-
#
-
# The block syntax also allows you to customize the part headers if desired:
-
#
-
# mail(to: 'mikel@test.lindsaar.net') do |format|
-
# format.text(content_transfer_encoding: "base64")
-
# format.html
-
# end
-
#
-
2
def mail(headers = {}, &block)
-
return @_message if @_mail_was_called && headers.blank? && !block
-
-
m = @_message
-
-
# At the beginning, do not consider class default for content_type
-
content_type = headers[:content_type]
-
-
# Call all the procs (if any)
-
default_values = {}
-
self.class.default.each do |k,v|
-
default_values[k] = v.is_a?(Proc) ? instance_eval(&v) : v
-
end
-
-
# Handle defaults
-
headers = headers.reverse_merge(default_values)
-
headers[:subject] ||= default_i18n_subject
-
-
# Apply charset at the beginning so all fields are properly quoted
-
m.charset = charset = headers[:charset]
-
-
# Set configure delivery behavior
-
wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options))
-
-
# Assign all headers except parts_order, content_type and body
-
assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
-
assignable.each { |k, v| m[k] = v }
-
-
# Render the templates and blocks
-
responses = collect_responses(headers, &block)
-
@_mail_was_called = true
-
-
create_parts_from_responses(m, responses)
-
-
# Setup content type, reapply charset and handle parts order
-
m.content_type = set_content_type(m, content_type, headers[:content_type])
-
m.charset = charset
-
-
if m.multipart?
-
m.body.set_sort_order(headers[:parts_order])
-
m.body.sort_parts!
-
end
-
-
m
-
end
-
-
2
protected
-
-
# Used by #mail to set the content type of the message.
-
#
-
# It will use the given +user_content_type+, or multipart if the mail
-
# message has any attachments. If the attachments are inline, the content
-
# type will be "multipart/related", otherwise "multipart/mixed".
-
#
-
# If there is no content type passed in via headers, and there are no
-
# attachments, or the message is multipart, then the default content type is
-
# used.
-
2
def set_content_type(m, user_content_type, class_default)
-
params = m.content_type_parameters || {}
-
case
-
when user_content_type.present?
-
user_content_type
-
when m.has_attachments?
-
if m.attachments.detect { |a| a.inline? }
-
["multipart", "related", params]
-
else
-
["multipart", "mixed", params]
-
end
-
when m.multipart?
-
["multipart", "alternative", params]
-
else
-
m.content_type || class_default
-
end
-
end
-
-
# Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope.
-
# If it does not find a translation for the +subject+ under the specified scope it will default to a
-
# humanized version of the <tt>action_name</tt>.
-
# If the subject has interpolations, you can pass them through the +interpolations+ parameter.
-
2
def default_i18n_subject(interpolations = {})
-
mailer_scope = self.class.mailer_name.tr('/', '.')
-
I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
-
end
-
-
2
def collect_responses(headers) #:nodoc:
-
responses = []
-
-
if block_given?
-
collector = ActionMailer::Collector.new(lookup_context) { render(action_name) }
-
yield(collector)
-
responses = collector.responses
-
elsif headers[:body]
-
responses << {
-
body: headers.delete(:body),
-
content_type: self.class.default[:content_type] || "text/plain"
-
}
-
else
-
templates_path = headers.delete(:template_path) || self.class.mailer_name
-
templates_name = headers.delete(:template_name) || action_name
-
-
each_template(Array(templates_path), templates_name) do |template|
-
self.formats = template.formats
-
-
responses << {
-
body: render(template: template),
-
content_type: template.type.to_s
-
}
-
end
-
end
-
-
responses
-
end
-
-
2
def each_template(paths, name, &block) #:nodoc:
-
templates = lookup_context.find_all(name, paths)
-
if templates.empty?
-
raise ActionView::MissingTemplate.new(paths, name, paths, false, 'mailer')
-
else
-
templates.uniq { |t| t.formats }.each(&block)
-
end
-
end
-
-
2
def create_parts_from_responses(m, responses) #:nodoc:
-
if responses.size == 1 && !m.has_attachments?
-
responses[0].each { |k,v| m[k] = v }
-
elsif responses.size > 1 && m.has_attachments?
-
container = Mail::Part.new
-
container.content_type = "multipart/alternative"
-
responses.each { |r| insert_part(container, r, m.charset) }
-
m.add_part(container)
-
else
-
responses.each { |r| insert_part(m, r, m.charset) }
-
end
-
end
-
-
2
def insert_part(container, response, charset) #:nodoc:
-
response[:charset] ||= charset
-
part = Mail::Part.new(response)
-
container.add_part(part)
-
end
-
-
# Emails do not support relative path links.
-
2
def self.supports_path?
-
false
-
end
-
-
2
ActiveSupport.run_load_hooks(:action_mailer, self)
-
end
-
end
-
2
require 'abstract_controller/collector'
-
2
require 'active_support/core_ext/hash/reverse_merge'
-
2
require 'active_support/core_ext/array/extract_options'
-
-
2
module ActionMailer
-
2
class Collector
-
2
include AbstractController::Collector
-
2
attr_reader :responses
-
-
2
def initialize(context, &block)
-
@context = context
-
@responses = []
-
@default_render = block
-
end
-
-
2
def any(*args, &block)
-
options = args.extract_options!
-
raise ArgumentError, "You have to supply at least one format" if args.empty?
-
args.each { |type| send(type, options.dup, &block) }
-
end
-
2
alias :all :any
-
-
2
def custom(mime, options = {})
-
options.reverse_merge!(content_type: mime.to_s)
-
@context.formats = [mime.to_sym]
-
options[:body] = block_given? ? yield : @default_render.call
-
@responses << options
-
end
-
end
-
end
-
2
require 'tmpdir'
-
-
2
module ActionMailer
-
# This module handles everything related to mail delivery, from registering
-
# new delivery methods to configuring the mail object to be sent.
-
2
module DeliveryMethods
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :delivery_methods, :delivery_method
-
-
# Do not make this inheritable, because we always want it to propagate
-
2
cattr_accessor :raise_delivery_errors
-
2
self.raise_delivery_errors = true
-
-
2
cattr_accessor :perform_deliveries
-
2
self.perform_deliveries = true
-
-
2
self.delivery_methods = {}.freeze
-
2
self.delivery_method = :smtp
-
-
2
add_delivery_method :smtp, Mail::SMTP,
-
address: "localhost",
-
port: 25,
-
domain: 'localhost.localdomain',
-
user_name: nil,
-
password: nil,
-
authentication: nil,
-
enable_starttls_auto: true
-
-
2
add_delivery_method :file, Mail::FileDelivery,
-
location: defined?(Rails.root) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails"
-
-
2
add_delivery_method :sendmail, Mail::Sendmail,
-
location: '/usr/sbin/sendmail',
-
arguments: '-i -t'
-
-
2
add_delivery_method :test, Mail::TestMailer
-
end
-
-
# Helpers for creating and wrapping delivery behavior, used by DeliveryMethods.
-
2
module ClassMethods
-
# Provides a list of emails that have been delivered by Mail::TestMailer
-
2
delegate :deliveries, :deliveries=, to: Mail::TestMailer
-
-
# Adds a new delivery method through the given class using the given
-
# symbol as alias and the default options supplied.
-
#
-
# add_delivery_method :sendmail, Mail::Sendmail,
-
# location: '/usr/sbin/sendmail',
-
# arguments: '-i -t'
-
2
def add_delivery_method(symbol, klass, default_options={})
-
8
class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
-
8
send(:"#{symbol}_settings=", default_options)
-
8
self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze
-
end
-
-
2
def wrap_delivery_behavior(mail, method=nil, options=nil) # :nodoc:
-
method ||= self.delivery_method
-
mail.delivery_handler = self
-
-
case method
-
when NilClass
-
raise "Delivery method cannot be nil"
-
when Symbol
-
if klass = delivery_methods[method]
-
mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
-
else
-
raise "Invalid delivery method #{method.inspect}"
-
end
-
else
-
mail.delivery_method(method)
-
end
-
-
mail.perform_deliveries = perform_deliveries
-
mail.raise_delivery_errors = raise_delivery_errors
-
end
-
end
-
-
2
def wrap_delivery_behavior!(*args) # :nodoc:
-
self.class.wrap_delivery_behavior(message, *args)
-
end
-
end
-
end
-
2
require 'active_support/log_subscriber'
-
-
2
module ActionMailer
-
# Implements the ActiveSupport::LogSubscriber for logging notifications when
-
# email is delivered and received.
-
2
class LogSubscriber < ActiveSupport::LogSubscriber
-
# An email was delivered.
-
2
def deliver(event)
-
info do
-
recipients = Array(event.payload[:to]).join(', ')
-
"\nSent mail to #{recipients} (#{event.duration.round(1)}ms)"
-
end
-
-
debug { event.payload[:mail] }
-
end
-
-
# An email was received.
-
2
def receive(event)
-
info { "\nReceived mail (#{event.duration.round(1)}ms)" }
-
debug { event.payload[:mail] }
-
end
-
-
# An email was generated.
-
2
def process(event)
-
debug do
-
mailer = event.payload[:mailer]
-
action = event.payload[:action]
-
"\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
-
end
-
end
-
-
# Use the logger configured for ActionMailer::Base
-
2
def logger
-
ActionMailer::Base.logger
-
end
-
end
-
end
-
-
2
ActionMailer::LogSubscriber.attach_to :action_mailer
-
2
module ActionMailer
-
# Provides helper methods for ActionMailer::Base that can be used for easily
-
# formatting messages, accessing mailer or message instances, and the
-
# attachments list.
-
2
module MailHelper
-
# Take the text and format it, indented two spaces for each line, and
-
# wrapped at 72 columns.
-
2
def block_format(text)
-
formatted = text.split(/\n\r?\n/).collect { |paragraph|
-
format_paragraph(paragraph)
-
}.join("\n\n")
-
-
# Make list points stand on their own line
-
formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" }
-
formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" }
-
-
formatted
-
end
-
-
# Access the mailer instance.
-
2
def mailer
-
@_controller
-
end
-
-
# Access the message instance.
-
2
def message
-
@_message
-
end
-
-
# Access the message attachments list.
-
2
def attachments
-
mailer.attachments
-
end
-
-
# Returns +text+ wrapped at +len+ columns and indented +indent+ spaces.
-
#
-
# my_text = 'Here is a sample text with more than 40 characters'
-
#
-
# format_paragraph(my_text, 25, 4)
-
# # => " Here is a sample text with\n more than 40 characters"
-
2
def format_paragraph(text, len = 72, indent = 2)
-
sentences = [[]]
-
-
text.split.each do |word|
-
if sentences.first.present? && (sentences.last + [word]).join(' ').length > len
-
sentences << [word]
-
else
-
sentences.last << word
-
end
-
end
-
-
indentation = " " * indent
-
sentences.map! { |sentence|
-
"#{indentation}#{sentence.join(' ')}"
-
}.join "\n"
-
end
-
end
-
end
-
2
require 'active_support/descendants_tracker'
-
-
2
module ActionMailer
-
2
module Previews #:nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
# Set the location of mailer previews through app configuration:
-
#
-
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
-
#
-
2
mattr_accessor :preview_path, instance_writer: false
-
-
# Enable or disable mailer previews through app configuration:
-
#
-
# config.action_mailer.show_previews = true
-
#
-
# Defaults to true for development environment
-
#
-
2
mattr_accessor :show_previews, instance_writer: false
-
-
# :nodoc:
-
2
mattr_accessor :preview_interceptors, instance_writer: false
-
2
self.preview_interceptors = []
-
end
-
-
2
module ClassMethods
-
# Register one or more Interceptors which will be called before mail is previewed.
-
2
def register_preview_interceptors(*interceptors)
-
2
interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
-
end
-
-
# Register an Interceptor which will be called before mail is previewed.
-
# Either a class or a string can be passed in as the Interceptor. If a
-
# string is passed in it will be <tt>constantize</tt>d.
-
2
def register_preview_interceptor(interceptor)
-
preview_interceptor = case interceptor
-
when String, Symbol
-
interceptor.to_s.camelize.constantize
-
else
-
interceptor
-
end
-
-
unless preview_interceptors.include?(preview_interceptor)
-
preview_interceptors << preview_interceptor
-
end
-
end
-
end
-
end
-
-
2
class Preview
-
2
extend ActiveSupport::DescendantsTracker
-
-
2
class << self
-
# Returns all mailer preview classes
-
2
def all
-
load_previews if descendants.empty?
-
descendants
-
end
-
-
# Returns the mail object for the given email name. The registered preview
-
# interceptors will be informed so that they can transform the message
-
# as they would if the mail was actually being delivered.
-
2
def call(email)
-
preview = self.new
-
message = preview.public_send(email)
-
inform_preview_interceptors(message)
-
message
-
end
-
-
# Returns all of the available email previews
-
2
def emails
-
public_instance_methods(false).map(&:to_s).sort
-
end
-
-
# Returns true if the email exists
-
2
def email_exists?(email)
-
emails.include?(email)
-
end
-
-
# Returns true if the preview exists
-
2
def exists?(preview)
-
all.any?{ |p| p.preview_name == preview }
-
end
-
-
# Find a mailer preview by its underscored class name
-
2
def find(preview)
-
all.find{ |p| p.preview_name == preview }
-
end
-
-
# Returns the underscored name of the mailer preview without the suffix
-
2
def preview_name
-
name.sub(/Preview$/, '').underscore
-
end
-
-
2
protected
-
2
def load_previews #:nodoc:
-
if preview_path
-
Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file }
-
end
-
end
-
-
2
def preview_path #:nodoc:
-
Base.preview_path
-
end
-
-
2
def show_previews #:nodoc:
-
Base.show_previews
-
end
-
-
2
def inform_preview_interceptors(message) #:nodoc:
-
Base.preview_interceptors.each do |interceptor|
-
interceptor.previewing_email(message)
-
end
-
end
-
end
-
end
-
end
-
2
module AbstractController
-
2
module AssetPaths #:nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
4
config_accessor :asset_host, :assets_dir, :javascripts_dir,
-
:stylesheets_dir, :default_asset_host_protocol, :relative_url_root
-
end
-
end
-
end
-
2
require 'erubis'
-
2
require 'set'
-
2
require 'active_support/configurable'
-
2
require 'active_support/descendants_tracker'
-
2
require 'active_support/core_ext/module/anonymous'
-
-
2
module AbstractController
-
2
class Error < StandardError #:nodoc:
-
end
-
-
# Raised when a non-existing controller action is triggered.
-
2
class ActionNotFound < StandardError
-
end
-
-
# <tt>AbstractController::Base</tt> is a low-level API. Nobody should be
-
# using it directly, and subclasses (like ActionController::Base) are
-
# expected to provide their own +render+ method, since rendering means
-
# different things depending on the context.
-
2
class Base
-
2
attr_internal :response_body
-
2
attr_internal :action_name
-
2
attr_internal :formats
-
-
2
include ActiveSupport::Configurable
-
2
extend ActiveSupport::DescendantsTracker
-
-
2
undef_method :not_implemented
-
2
class << self
-
2
attr_reader :abstract
-
2
alias_method :abstract?, :abstract
-
-
# Define a controller as abstract. See internal_methods for more
-
# details.
-
2
def abstract!
-
8
@abstract = true
-
end
-
-
2
def inherited(klass) # :nodoc:
-
# Define the abstract ivar on subclasses so that we don't get
-
# uninitialized ivar warnings
-
16
unless klass.instance_variable_defined?(:@abstract)
-
16
klass.instance_variable_set(:@abstract, false)
-
end
-
16
super
-
end
-
-
# A list of all internal methods for a controller. This finds the first
-
# abstract superclass of a controller, and gets a list of all public
-
# instance methods on that abstract class. Public instance methods of
-
# a controller would normally be considered action methods, so methods
-
# declared on abstract classes are being removed.
-
# (ActionController::Metal and ActionController::Base are defined as abstract)
-
2
def internal_methods
-
5
controller = self
-
-
5
controller = controller.superclass until controller.abstract?
-
5
controller.public_instance_methods(true)
-
end
-
-
# The list of hidden actions. Defaults to an empty array.
-
# This can be modified by other modules or subclasses
-
# to specify particular actions as hidden.
-
#
-
# ==== Returns
-
# * <tt>Array</tt> - An array of method names that should not be considered actions.
-
2
def hidden_actions
-
2
[]
-
end
-
-
# A list of method names that should be considered actions. This
-
# includes all public instance methods on a controller, less
-
# any internal methods (see #internal_methods), adding back in
-
# any methods that are internal, but still exist on the class
-
# itself. Finally, #hidden_actions are removed.
-
#
-
# ==== Returns
-
# * <tt>Set</tt> - A set of all methods that should be considered actions.
-
2
def action_methods
-
@action_methods ||= begin
-
# All public instance methods of this class, including ancestors
-
5
methods = (public_instance_methods(true) -
-
# Except for public instance methods of Base and its ancestors
-
internal_methods +
-
# Be sure to include shadowed public instance methods of this class
-
330
public_instance_methods(false)).uniq.map { |x| x.to_s } -
-
# And always exclude explicitly hidden actions
-
hidden_actions.to_a
-
-
# Clear out AS callback method pollution
-
330
Set.new(methods.reject { |method| method =~ /_one_time_conditions/ })
-
5
end
-
end
-
-
# action_methods are cached and there is sometimes need to refresh
-
# them. clear_action_methods! allows you to do that, so next time
-
# you run action_methods, they will be recalculated
-
2
def clear_action_methods!
-
539
@action_methods = nil
-
end
-
-
# Returns the full controller name, underscored, without the ending Controller.
-
# For instance, MyApp::MyPostsController would return "my_app/my_posts" for
-
# controller_path.
-
#
-
# ==== Returns
-
# * <tt>String</tt>
-
2
def controller_path
-
59
@controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous?
-
end
-
-
# Refresh the cached action_methods when a new action_method is added.
-
2
def method_added(name)
-
539
super
-
539
clear_action_methods!
-
end
-
end
-
-
2
abstract!
-
-
# Calls the action going through the entire action dispatch stack.
-
#
-
# The actual method that is called is determined by calling
-
# #method_for_action. If no method can handle the action, then an
-
# AbstractController::ActionNotFound error is raised.
-
#
-
# ==== Returns
-
# * <tt>self</tt>
-
2
def process(action, *args)
-
29
@_action_name = action.to_s
-
-
29
unless action_name = _find_action_name(@_action_name)
-
raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}"
-
end
-
-
29
@_response_body = nil
-
-
29
process_action(action_name, *args)
-
end
-
-
# Delegates to the class' #controller_path
-
2
def controller_path
-
self.class.controller_path
-
end
-
-
# Delegates to the class' #action_methods
-
2
def action_methods
-
self.class.action_methods
-
end
-
-
# Returns true if a method for the action is available and
-
# can be dispatched, false otherwise.
-
#
-
# Notice that <tt>action_methods.include?("foo")</tt> may return
-
# false and <tt>available_action?("foo")</tt> returns true because
-
# this method considers actions that are also available
-
# through other means, for example, implicit render ones.
-
#
-
# ==== Parameters
-
# * <tt>action_name</tt> - The name of an action to be tested
-
#
-
# ==== Returns
-
# * <tt>TrueClass</tt>, <tt>FalseClass</tt>
-
2
def available_action?(action_name)
-
_find_action_name(action_name).present?
-
end
-
-
# Returns true if the given controller is capable of rendering
-
# a path. A subclass of +AbstractController::Base+
-
# may return false. An Email controller for example does not
-
# support paths, only full URLs.
-
2
def self.supports_path?
-
3
true
-
end
-
-
2
private
-
-
# Returns true if the name can be considered an action because
-
# it has a method defined in the controller.
-
#
-
# ==== Parameters
-
# * <tt>name</tt> - The name of an action to be tested
-
#
-
# ==== Returns
-
# * <tt>TrueClass</tt>, <tt>FalseClass</tt>
-
#
-
# :api: private
-
2
def action_method?(name)
-
29
self.class.action_methods.include?(name)
-
end
-
-
# Call the action. Override this in a subclass to modify the
-
# behavior around processing an action. This, and not #process,
-
# is the intended way to override action dispatching.
-
#
-
# Notice that the first argument is the method to be dispatched
-
# which is *not* necessarily the same as the action name.
-
2
def process_action(method_name, *args)
-
29
send_action(method_name, *args)
-
end
-
-
# Actually call the method associated with the action. Override
-
# this method if you wish to change how action methods are called,
-
# not to add additional behavior around it. For example, you would
-
# override #send_action if you want to inject arguments into the
-
# method.
-
2
alias send_action send
-
-
# If the action name was not found, but a method called "action_missing"
-
# was found, #method_for_action will return "_handle_action_missing".
-
# This method calls #action_missing with the current action name.
-
2
def _handle_action_missing(*args)
-
action_missing(@_action_name, *args)
-
end
-
-
# Takes an action name and returns the name of the method that will
-
# handle the action.
-
#
-
# It checks if the action name is valid and returns false otherwise.
-
#
-
# See method_for_action for more information.
-
#
-
# ==== Parameters
-
# * <tt>action_name</tt> - An action name to find a method name for
-
#
-
# ==== Returns
-
# * <tt>string</tt> - The name of the method that handles the action
-
# * false - No valid method name could be found.
-
# Raise AbstractController::ActionNotFound.
-
2
def _find_action_name(action_name)
-
29
_valid_action_name?(action_name) && method_for_action(action_name)
-
end
-
-
# Takes an action name and returns the name of the method that will
-
# handle the action. In normal cases, this method returns the same
-
# name as it receives. By default, if #method_for_action receives
-
# a name that is not an action, it will look for an #action_missing
-
# method and return "_handle_action_missing" if one is found.
-
#
-
# Subclasses may override this method to add additional conditions
-
# that should be considered an action. For instance, an HTTP controller
-
# with a template matching the action name is considered to exist.
-
#
-
# If you override this method to handle additional cases, you may
-
# also provide a method (like _handle_method_missing) to handle
-
# the case.
-
#
-
# If none of these conditions are true, and method_for_action
-
# returns nil, an AbstractController::ActionNotFound exception will be raised.
-
#
-
# ==== Parameters
-
# * <tt>action_name</tt> - An action name to find a method name for
-
#
-
# ==== Returns
-
# * <tt>string</tt> - The name of the method that handles the action
-
# * <tt>nil</tt> - No method name could be found.
-
2
def method_for_action(action_name)
-
29
if action_method?(action_name)
-
29
action_name
-
elsif respond_to?(:action_missing, true)
-
"_handle_action_missing"
-
end
-
end
-
-
# Checks if the action name is valid and returns false otherwise.
-
2
def _valid_action_name?(action_name)
-
29
!action_name.to_s.include? File::SEPARATOR
-
end
-
end
-
end
-
2
module AbstractController
-
2
module Callbacks
-
2
extend ActiveSupport::Concern
-
-
# Uses ActiveSupport::Callbacks as the base functionality. For
-
# more details on the whole callback system, read the documentation
-
# for ActiveSupport::Callbacks.
-
2
include ActiveSupport::Callbacks
-
-
2
included do
-
4
define_callbacks :process_action,
-
135
terminator: ->(controller,_) { controller.response_body },
-
skip_after_callbacks_if_terminated: true
-
end
-
-
# Override AbstractController::Base's process_action to run the
-
# process_action callbacks around the normal behavior.
-
2
def process_action(*args)
-
29
run_callbacks(:process_action) do
-
29
super
-
end
-
end
-
-
2
module ClassMethods
-
# If :only or :except are used, convert the options into the
-
# :unless and :if options of ActiveSupport::Callbacks.
-
# The basic idea is that :only => :index gets converted to
-
# :if => proc {|c| c.action_name == "index" }.
-
#
-
# ==== Options
-
# * <tt>only</tt> - The callback should be run only for this action
-
# * <tt>except</tt> - The callback should be run for all actions except this action
-
2
def _normalize_callback_options(options)
-
23
_normalize_callback_option(options, :only, :if)
-
23
_normalize_callback_option(options, :except, :unless)
-
end
-
-
2
def _normalize_callback_option(options, from, to) # :nodoc:
-
46
if from = options[from]
-
50
from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ")
-
12
options[to] = Array(options[to]).unshift(from)
-
end
-
end
-
-
# Skip before, after, and around action callbacks matching any of the names.
-
#
-
# ==== Parameters
-
# * <tt>names</tt> - A list of valid names that could be used for
-
# callbacks. Note that skipping uses Ruby equality, so it's
-
# impossible to skip a callback defined using an anonymous proc
-
# using #skip_action_callback
-
2
def skip_action_callback(*names)
-
skip_before_action(*names)
-
skip_after_action(*names)
-
skip_around_action(*names)
-
end
-
2
alias_method :skip_filter, :skip_action_callback
-
-
# Take callback names and an optional callback proc, normalize them,
-
# then call the block with each callback. This allows us to abstract
-
# the normalization across several methods that use it.
-
#
-
# ==== Parameters
-
# * <tt>callbacks</tt> - An array of callbacks, with an optional
-
# options hash as the last parameter.
-
# * <tt>block</tt> - A proc that should be added to the callbacks.
-
#
-
# ==== Block Parameters
-
# * <tt>name</tt> - The callback to be added
-
# * <tt>options</tt> - A hash of options to be used when adding the callback
-
2
def _insert_callbacks(callbacks, block = nil)
-
23
options = callbacks.extract_options!
-
23
_normalize_callback_options(options)
-
23
callbacks.push(block) if block
-
23
callbacks.each do |callback|
-
25
yield callback, options
-
end
-
end
-
-
##
-
# :method: before_action
-
#
-
# :call-seq: before_action(names, block)
-
#
-
# Append a callback before actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: prepend_before_action
-
#
-
# :call-seq: prepend_before_action(names, block)
-
#
-
# Prepend a callback before actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: skip_before_action
-
#
-
# :call-seq: skip_before_action(names)
-
#
-
# Skip a callback before actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: append_before_action
-
#
-
# :call-seq: append_before_action(names, block)
-
#
-
# Append a callback before actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: after_action
-
#
-
# :call-seq: after_action(names, block)
-
#
-
# Append a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: prepend_after_action
-
#
-
# :call-seq: prepend_after_action(names, block)
-
#
-
# Prepend a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: skip_after_action
-
#
-
# :call-seq: skip_after_action(names)
-
#
-
# Skip a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: append_after_action
-
#
-
# :call-seq: append_after_action(names, block)
-
#
-
# Append a callback after actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: around_action
-
#
-
# :call-seq: around_action(names, block)
-
#
-
# Append a callback around actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: prepend_around_action
-
#
-
# :call-seq: prepend_around_action(names, block)
-
#
-
# Prepend a callback around actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: skip_around_action
-
#
-
# :call-seq: skip_around_action(names)
-
#
-
# Skip a callback around actions. See _insert_callbacks for parameter details.
-
-
##
-
# :method: append_around_action
-
#
-
# :call-seq: append_around_action(names, block)
-
#
-
# Append a callback around actions. See _insert_callbacks for parameter details.
-
-
# set up before_action, prepend_before_action, skip_before_action, etc.
-
# for each of before, after, and around.
-
2
[:before, :after, :around].each do |callback|
-
6
define_method "#{callback}_action" do |*names, &blk|
-
20
_insert_callbacks(names, blk) do |name, options|
-
22
set_callback(:process_action, callback, name, options)
-
end
-
end
-
6
alias_method :"#{callback}_filter", :"#{callback}_action"
-
-
6
define_method "prepend_#{callback}_action" do |*names, &blk|
-
3
_insert_callbacks(names, blk) do |name, options|
-
3
set_callback(:process_action, callback, name, options.merge(:prepend => true))
-
end
-
end
-
6
alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action"
-
-
# Skip a before, after or around callback. See _insert_callbacks
-
# for details on the allowed parameters.
-
6
define_method "skip_#{callback}_action" do |*names|
-
_insert_callbacks(names) do |name, options|
-
skip_callback(:process_action, callback, name, options)
-
end
-
end
-
6
alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action"
-
-
# *_action is the same as append_*_action
-
6
alias_method :"append_#{callback}_action", :"#{callback}_action"
-
6
alias_method :"append_#{callback}_filter", :"#{callback}_action"
-
end
-
end
-
end
-
end
-
2
require "action_dispatch/http/mime_type"
-
-
2
module AbstractController
-
2
module Collector
-
2
def self.generate_method_for_mime(mime)
-
44
sym = mime.is_a?(Symbol) ? mime : mime.to_sym
-
44
const = sym.upcase
-
44
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{sym}(*args, &block) # def html(*args, &block)
-
custom(Mime::#{const}, *args, &block) # custom(Mime::HTML, *args, &block)
-
end # end
-
RUBY
-
end
-
-
2
Mime::SET.each do |mime|
-
44
generate_method_for_mime(mime)
-
end
-
-
2
Mime::Type.register_callback do |mime|
-
generate_method_for_mime(mime) unless self.instance_methods.include?(mime.to_sym)
-
end
-
-
2
protected
-
-
2
def method_missing(symbol, &block)
-
const_name = symbol.upcase
-
-
unless Mime.const_defined?(const_name)
-
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
-
"http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
-
"If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
-
"be sure to nest your variant response within a format response: " \
-
"format.html { |html| html.tablet { ... } }"
-
end
-
-
mime_constant = Mime.const_get(const_name)
-
-
if Mime::SET.include?(mime_constant)
-
AbstractController::Collector.generate_method_for_mime(mime_constant)
-
send(symbol, &block)
-
else
-
super
-
end
-
end
-
end
-
end
-
2
require 'active_support/dependencies'
-
-
2
module AbstractController
-
2
module Helpers
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
4
class_attribute :_helpers
-
4
self._helpers = Module.new
-
-
4
class_attribute :_helper_methods
-
4
self._helper_methods = Array.new
-
end
-
-
2
class MissingHelperError < LoadError
-
2
def initialize(error, path)
-
@error = error
-
@path = "helpers/#{path}.rb"
-
set_backtrace error.backtrace
-
-
if error.path =~ /^#{path}(\.rb)?$/
-
super("Missing helper file helpers/%s.rb" % path)
-
else
-
raise error
-
end
-
end
-
end
-
-
2
module ClassMethods
-
# When a class is inherited, wrap its helper module in a new module.
-
# This ensures that the parent class's module can be changed
-
# independently of the child class's.
-
2
def inherited(klass)
-
8
helpers = _helpers
-
16
klass._helpers = Module.new { include helpers }
-
16
klass.class_eval { default_helper_module! } unless klass.anonymous?
-
8
super
-
end
-
-
# Declare a controller method as a helper. For example, the following
-
# makes the +current_user+ controller method available to the view:
-
# class ApplicationController < ActionController::Base
-
# helper_method :current_user, :logged_in?
-
#
-
# def current_user
-
# @current_user ||= User.find_by(id: session[:user])
-
# end
-
#
-
# def logged_in?
-
# current_user != nil
-
# end
-
# end
-
#
-
# In a view:
-
# <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
-
#
-
# ==== Parameters
-
# * <tt>method[, method]</tt> - A name or names of a method on the controller
-
# to be made available on the view.
-
2
def helper_method(*meths)
-
17
meths.flatten!
-
17
self._helper_methods += meths
-
-
17
meths.each do |meth|
-
31
_helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
-
def #{meth}(*args, &blk) # def current_user(*args, &blk)
-
controller.send(%(#{meth}), *args, &blk) # controller.send(:current_user, *args, &blk)
-
end # end
-
ruby_eval
-
end
-
end
-
-
# The +helper+ class method can take a series of helper module names, a block, or both.
-
#
-
# ==== Options
-
# * <tt>*args</tt> - Module, Symbol, String
-
# * <tt>block</tt> - A block defining helper methods
-
#
-
# When the argument is a module it will be included directly in the template class.
-
# helper FooHelper # => includes FooHelper
-
#
-
# When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file
-
# and include the module in the template class. The second form illustrates how to include custom helpers
-
# when working with namespaced controllers, or other cases where the file containing the helper definition is not
-
# in one of Rails' standard load paths:
-
# helper :foo # => requires 'foo_helper' and includes FooHelper
-
# helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper
-
#
-
# Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available
-
# to the template.
-
#
-
# # One line
-
# helper { def hello() "Hello, world!" end }
-
#
-
# # Multi-line
-
# helper do
-
# def foo(bar)
-
# "#{bar} is the very best"
-
# end
-
# end
-
#
-
# Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of
-
# +symbols+, +strings+, +modules+ and blocks.
-
#
-
# helper(:three, BlindHelper) { def mice() 'mice' end }
-
#
-
2
def helper(*args, &block)
-
13
modules_for_helpers(args).each do |mod|
-
23
add_template_helper(mod)
-
end
-
-
13
_helpers.module_eval(&block) if block_given?
-
end
-
-
# Clears up all existing helpers in this class, only keeping the helper
-
# with the same name as this class.
-
2
def clear_helpers
-
inherited_helper_methods = _helper_methods
-
self._helpers = Module.new
-
self._helper_methods = Array.new
-
-
inherited_helper_methods.each { |meth| helper_method meth }
-
default_helper_module! unless anonymous?
-
end
-
-
# Returns a list of modules, normalized from the acceptable kinds of
-
# helpers with the following behavior:
-
#
-
# String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper",
-
# and "foo_bar_helper.rb" is loaded using require_dependency.
-
#
-
# Module:: No further processing
-
#
-
# After loading the appropriate files, the corresponding modules
-
# are returned.
-
#
-
# ==== Parameters
-
# * <tt>args</tt> - An array of helpers
-
#
-
# ==== Returns
-
# * <tt>Array</tt> - A normalized list of modules for the list of
-
# helpers provided.
-
2
def modules_for_helpers(args)
-
13
args.flatten.map! do |arg|
-
23
case arg
-
when String, Symbol
-
20
file_name = "#{arg.to_s.underscore}_helper"
-
20
begin
-
20
require_dependency(file_name)
-
rescue LoadError => e
-
raise AbstractController::Helpers::MissingHelperError.new(e, file_name)
-
end
-
-
20
mod_name = file_name.camelize
-
20
begin
-
20
mod_name.constantize
-
rescue LoadError
-
# dependencies.rb gives a similar error message but its wording is
-
# not as clear because it mentions autoloading. To the user all it
-
# matters is that a helper module couldn't be loaded, autoloading
-
# is an internal mechanism that should not leak.
-
raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb"
-
end
-
when Module
-
3
arg
-
else
-
raise ArgumentError, "helper must be a String, Symbol, or Module"
-
end
-
end
-
end
-
-
2
private
-
# Makes all the (instance) methods in the helper module available to templates
-
# rendered through this controller.
-
#
-
# ==== Parameters
-
# * <tt>module</tt> - The module to include into the current helper module
-
# for the class
-
2
def add_template_helper(mod)
-
46
_helpers.module_eval { include mod }
-
end
-
-
2
def default_helper_module!
-
8
module_name = name.sub(/Controller$/, '')
-
8
module_path = module_name.underscore
-
8
helper module_path
-
rescue MissingSourceFile => e
-
raise e unless e.is_missing? "helpers/#{module_path}_helper"
-
rescue NameError => e
-
raise e unless e.missing_name? "#{module_name}Helper"
-
end
-
end
-
end
-
end
-
2
require "active_support/benchmarkable"
-
-
2
module AbstractController
-
2
module Logger #:nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
6
config_accessor :logger
-
6
include ActiveSupport::Benchmarkable
-
end
-
end
-
end
-
2
require 'active_support/concern'
-
2
require 'active_support/core_ext/class/attribute'
-
2
require 'action_view'
-
2
require 'action_view/view_paths'
-
2
require 'set'
-
-
2
module AbstractController
-
2
class DoubleRenderError < Error
-
2
DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"."
-
-
2
def initialize(message = nil)
-
super(message || DEFAULT_MESSAGE)
-
end
-
end
-
-
2
module Rendering
-
2
extend ActiveSupport::Concern
-
2
include ActionView::ViewPaths
-
-
# Normalize arguments, options and then delegates render_to_body and
-
# sticks the result in self.response_body.
-
# :api: public
-
2
def render(*args, &block)
-
16
options = _normalize_render(*args, &block)
-
16
self.response_body = render_to_body(options)
-
16
_process_format(rendered_format, options) if rendered_format
-
16
self.response_body
-
end
-
-
# Raw rendering of a template to a string.
-
#
-
# It is similar to render, except that it does not
-
# set the response_body and it should be guaranteed
-
# to always return a string.
-
#
-
# If a component extends the semantics of response_body
-
# (as Action Controller extends it to be anything that
-
# responds to the method each), this method needs to be
-
# overridden in order to still return a string.
-
# :api: plugin
-
2
def render_to_string(*args, &block)
-
options = _normalize_render(*args, &block)
-
render_to_body(options)
-
end
-
-
# Performs the actual template rendering.
-
# :api: public
-
2
def render_to_body(options = {})
-
end
-
-
# Returns Content-Type of rendered content
-
# :api: public
-
2
def rendered_format
-
Mime::TEXT
-
end
-
-
2
DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w(
-
@_action_name @_response_body @_formats @_prefixes @_config
-
@_view_context_class @_view_renderer @_lookup_context
-
@_routes @_db_runtime
-
).map(&:to_sym)
-
-
# This method should return a hash with assigns.
-
# You can overwrite this configuration per controller.
-
# :api: public
-
2
def view_assigns
-
50
protected_vars = _protected_ivars
-
50
variables = instance_variables
-
-
972
variables.reject! { |s| protected_vars.include? s }
-
50
variables.each_with_object({}) { |name, hash|
-
152
hash[name.slice(1, name.length)] = instance_variable_get(name)
-
}
-
end
-
-
# Normalize args by converting render "foo" to render :action => "foo" and
-
# render "foo/bar" to render :file => "foo/bar".
-
# :api: plugin
-
2
def _normalize_args(action=nil, options={})
-
16
if action.respond_to?(:permitted?)
-
if action.permitted?
-
action
-
else
-
raise ArgumentError, "render parameters are not permitted"
-
end
-
16
elsif action.is_a?(Hash)
-
action
-
else
-
16
options
-
end
-
end
-
-
# Normalize options.
-
# :api: plugin
-
2
def _normalize_options(options)
-
16
options
-
end
-
-
# Process extra options.
-
# :api: plugin
-
2
def _process_options(options)
-
16
options
-
end
-
-
# Process the rendered format.
-
# :api: private
-
2
def _process_format(format, options = {})
-
end
-
-
# Normalize args and options.
-
# :api: private
-
2
def _normalize_render(*args, &block)
-
16
options = _normalize_args(*args, &block)
-
#TODO: remove defined? when we restore AP <=> AV dependency
-
16
if defined?(request) && request && request.variant.present?
-
options[:variant] = request.variant
-
end
-
16
_normalize_options(options)
-
16
options
-
end
-
-
2
def _protected_ivars # :nodoc:
-
DEFAULT_PROTECTED_INSTANCE_VARIABLES
-
end
-
end
-
end
-
2
module AbstractController
-
2
module Translation
-
# Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>.
-
#
-
# When the given key starts with a period, it will be scoped by the current
-
# controller and action. So if you call <tt>translate(".foo")</tt> from
-
# <tt>PeopleController#index</tt>, it will convert the call to
-
# <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
-
# to translate many keys within the same controller / action and gives you a
-
# simple framework for scoping them consistently.
-
2
def translate(*args)
-
key = args.first
-
if key.is_a?(String) && (key[0] == '.')
-
key = "#{ controller_path.tr('/', '.') }.#{ action_name }#{ key }"
-
args[0] = key
-
end
-
-
I18n.translate(*args)
-
end
-
2
alias :t :translate
-
-
# Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
-
2
def localize(*args)
-
I18n.localize(*args)
-
end
-
2
alias :l :localize
-
end
-
end
-
2
module AbstractController
-
# Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class
-
# has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an
-
# exception will be raised.
-
#
-
# Note that this module is completely decoupled from HTTP - the only requirement is a valid
-
# <tt>_routes</tt> implementation.
-
2
module UrlFor
-
2
extend ActiveSupport::Concern
-
2
include ActionDispatch::Routing::UrlFor
-
-
2
def _routes
-
raise "In order to use #url_for, you must include routing helpers explicitly. " \
-
"For instance, `include Rails.application.routes.url_helpers`."
-
end
-
-
2
module ClassMethods
-
2
def _routes
-
nil
-
end
-
-
2
def action_methods
-
@action_methods ||= begin
-
3
if _routes
-
3
super - _routes.named_routes.helper_names
-
else
-
super
-
end
-
29
end
-
end
-
end
-
end
-
end
-
2
require 'action_view'
-
2
require "action_controller/log_subscriber"
-
2
require "action_controller/metal/params_wrapper"
-
-
2
module ActionController
-
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
-
# on request and then either it renders a template or redirects to another action. An action is defined as a public method
-
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
-
#
-
# By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
-
# controllers in turn inherit from ApplicationController. This gives you one class to configure things such as
-
# request forgery protection and filtering of sensitive request parameters.
-
#
-
# A sample controller could look like this:
-
#
-
# class PostsController < ApplicationController
-
# def index
-
# @posts = Post.all
-
# end
-
#
-
# def create
-
# @post = Post.create params[:post]
-
# redirect_to posts_path
-
# end
-
# end
-
#
-
# Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action
-
# after executing code in the action. For example, the +index+ action of the PostsController would render the
-
# template <tt>app/views/posts/index.html.erb</tt> by default after populating the <tt>@posts</tt> instance variable.
-
#
-
# Unlike index, the create action will not render a template. After performing its main purpose (creating a
-
# new post), it initiates a redirect instead. This redirect works by returning an external
-
# "302 Moved" HTTP response that takes the user to the index action.
-
#
-
# These two methods represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect.
-
# Most actions are variations on these themes.
-
#
-
# == Requests
-
#
-
# For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller
-
# and action are called. The remaining request parameters, the session (if one is available), and the full request with
-
# all the HTTP headers are made available to the action through accessor methods. Then the action is performed.
-
#
-
# The full request object is available via the request accessor and is primarily used to query for HTTP headers:
-
#
-
# def server_ip
-
# location = request.env["REMOTE_ADDR"]
-
# render plain: "This server hosted at #{location}"
-
# end
-
#
-
# == Parameters
-
#
-
# All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method
-
# which returns a hash. For example, an action that was performed through <tt>/posts?category=All&limit=5</tt> will include
-
# <tt>{ "category" => "All", "limit" => "5" }</tt> in params.
-
#
-
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
-
#
-
# <input type="text" name="post[name]" value="david">
-
# <input type="text" name="post[address]" value="hyacintvej">
-
#
-
# A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
-
# If the address input had been named <tt>post[address][street]</tt>, the params would have included
-
# <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
-
#
-
# == Sessions
-
#
-
# Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
-
# such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
-
# as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
-
# they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
-
#
-
# You can place objects in the session by using the <tt>session</tt> method, which accesses a hash:
-
#
-
# session[:person] = Person.authenticate(user_name, password)
-
#
-
# And retrieved again through the same hash:
-
#
-
# Hello #{session[:person]}
-
#
-
# For removing objects from the session, you can either assign a single key to +nil+:
-
#
-
# # removes :person from session
-
# session[:person] = nil
-
#
-
# or you can remove the entire session with +reset_session+.
-
#
-
# Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
-
# This prevents the user from tampering with the session but also allows them to see its contents.
-
#
-
# Do not put secret information in cookie-based sessions!
-
#
-
# == Responses
-
#
-
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
-
# object is generated automatically through the use of renders and redirects and requires no user intervention.
-
#
-
# == Renders
-
#
-
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
-
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERB templates. It's automatically configured.
-
# The controller passes objects to the view by assigning instance variables:
-
#
-
# def show
-
# @post = Post.find(params[:id])
-
# end
-
#
-
# Which are then automatically available to the view:
-
#
-
# Title: <%= @post.title %>
-
#
-
# You don't have to rely on the automated rendering. For example, actions that could result in the rendering of different templates
-
# will use the manual rendering methods:
-
#
-
# def search
-
# @results = Search.find(params[:query])
-
# case @results.count
-
# when 0 then render action: "no_results"
-
# when 1 then render action: "show"
-
# when 2..10 then render action: "show_many"
-
# end
-
# end
-
#
-
# Read more about writing ERB and Builder templates in ActionView::Base.
-
#
-
# == Redirects
-
#
-
# Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to the
-
# database, we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're
-
# going to reuse (and redirect to) a <tt>show</tt> action that we'll assume has already been created. The code might look like this:
-
#
-
# def create
-
# @entry = Entry.new(params[:entry])
-
# if @entry.save
-
# # The entry was saved correctly, redirect to show
-
# redirect_to action: 'show', id: @entry.id
-
# else
-
# # things didn't go so well, do something else
-
# end
-
# end
-
#
-
# In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method, which is then executed.
-
# Note that this is an external HTTP-level redirection which will cause the browser to make a second request (a GET to the show action),
-
# and not some internal re-routing which calls both "create" and then "show" within one request.
-
#
-
# Learn more about <tt>redirect_to</tt> and what options you have in ActionController::Redirecting.
-
#
-
# == Calling multiple redirects or renders
-
#
-
# An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError:
-
#
-
# def do_something
-
# redirect_to action: "elsewhere"
-
# render action: "overthere" # raises DoubleRenderError
-
# end
-
#
-
# If you need to redirect on the condition of something, then be sure to add "and return" to halt execution.
-
#
-
# def do_something
-
# redirect_to(action: "elsewhere") and return if monkeys.nil?
-
# render action: "overthere" # won't be called if monkeys is nil
-
# end
-
#
-
2
class Base < Metal
-
2
abstract!
-
-
# We document the request and response methods here because albeit they are
-
# implemented in ActionController::Metal, the type of the returned objects
-
# is unknown at that level.
-
-
##
-
# :method: request
-
#
-
# Returns an ActionDispatch::Request instance that represents the
-
# current request.
-
-
##
-
# :method: response
-
#
-
# Returns an ActionDispatch::Response that represents the current
-
# response.
-
-
# Shortcut helper that returns all the modules included in
-
# ActionController::Base except the ones passed as arguments:
-
#
-
# class MyBaseController < ActionController::Metal
-
# ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
-
# include left
-
# end
-
# end
-
#
-
# This gives better control over what you want to exclude and makes it
-
# easier to create a bare controller class, instead of listing the modules
-
# required manually.
-
2
def self.without_modules(*modules)
-
modules = modules.map do |m|
-
m.is_a?(Symbol) ? ActionController.const_get(m) : m
-
end
-
-
MODULES - modules
-
end
-
-
2
MODULES = [
-
AbstractController::Rendering,
-
AbstractController::Translation,
-
AbstractController::AssetPaths,
-
-
Helpers,
-
HideActions,
-
UrlFor,
-
Redirecting,
-
ActionView::Layouts,
-
Rendering,
-
Renderers::All,
-
ConditionalGet,
-
EtagWithTemplateDigest,
-
RackDelegation,
-
Caching,
-
MimeResponds,
-
ImplicitRender,
-
StrongParameters,
-
-
Cookies,
-
Flash,
-
RequestForgeryProtection,
-
ForceSSL,
-
Streaming,
-
DataStreaming,
-
HttpAuthentication::Basic::ControllerMethods,
-
HttpAuthentication::Digest::ControllerMethods,
-
HttpAuthentication::Token::ControllerMethods,
-
-
# Before callbacks should also be executed the earliest as possible, so
-
# also include them at the bottom.
-
AbstractController::Callbacks,
-
-
# Append rescue at the bottom to wrap as much as possible.
-
Rescue,
-
-
# Add instrumentations hooks at the bottom, to ensure they instrument
-
# all the methods properly.
-
Instrumentation,
-
-
# Params wrapper should come before instrumentation so they are
-
# properly showed in logs
-
ParamsWrapper
-
]
-
-
2
MODULES.each do |mod|
-
60
include mod
-
end
-
-
# Define some internal variables that should not be propagated to the view.
-
2
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [
-
:@_status, :@_headers, :@_params, :@_env, :@_response, :@_request,
-
:@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ]
-
-
2
def _protected_ivars # :nodoc:
-
50
PROTECTED_IVARS
-
end
-
-
2
def self.protected_instance_variables
-
PROTECTED_IVARS
-
end
-
-
2
ActiveSupport.run_load_hooks(:action_controller, self)
-
end
-
end
-
2
require 'fileutils'
-
2
require 'uri'
-
2
require 'set'
-
-
2
module ActionController
-
# \Caching is a cheap way of speeding up slow applications by keeping the result of
-
# calculations, renderings, and database calls around for subsequent requests.
-
#
-
# You can read more about each approach by clicking the modules below.
-
#
-
# Note: To turn off all caching, set
-
# config.action_controller.perform_caching = false
-
#
-
# == \Caching stores
-
#
-
# All the caching stores from ActiveSupport::Cache are available to be used as backends
-
# for Action Controller caching.
-
#
-
# Configuration examples (FileStore is the default):
-
#
-
# config.action_controller.cache_store = :memory_store
-
# config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
-
# config.action_controller.cache_store = :mem_cache_store, 'localhost'
-
# config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211')
-
# config.action_controller.cache_store = MyOwnStore.new('parameter')
-
2
module Caching
-
2
extend ActiveSupport::Concern
-
2
extend ActiveSupport::Autoload
-
-
2
eager_autoload do
-
2
autoload :Fragments
-
end
-
-
2
module ConfigMethods
-
2
def cache_store
-
config.cache_store
-
end
-
-
2
def cache_store=(store)
-
2
config.cache_store = ActiveSupport::Cache.lookup_store(store)
-
end
-
-
2
private
-
2
def cache_configured?
-
perform_caching && cache_store
-
end
-
end
-
-
2
include RackDelegation
-
2
include AbstractController::Callbacks
-
-
2
include ConfigMethods
-
2
include Fragments
-
-
2
included do
-
2
extend ConfigMethods
-
-
2
config_accessor :default_static_extension
-
2
self.default_static_extension ||= '.html'
-
-
2
config_accessor :perform_caching
-
2
self.perform_caching = true if perform_caching.nil?
-
-
2
class_attribute :_view_cache_dependencies
-
2
self._view_cache_dependencies = []
-
2
helper_method :view_cache_dependencies if respond_to?(:helper_method)
-
end
-
-
2
module ClassMethods
-
2
def view_cache_dependency(&dependency)
-
self._view_cache_dependencies += [dependency]
-
end
-
end
-
-
2
def view_cache_dependencies
-
self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
-
end
-
-
2
protected
-
# Convenience accessor.
-
2
def cache(key, options = {}, &block)
-
if cache_configured?
-
cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
-
else
-
yield
-
end
-
end
-
end
-
end
-
2
module ActionController
-
2
module Caching
-
# Fragment caching is used for caching various blocks within
-
# views without caching the entire action as a whole. This is
-
# useful when certain elements of an action change frequently or
-
# depend on complicated state while other parts rarely change or
-
# can be shared amongst multiple parties. The caching is done using
-
# the +cache+ helper available in the Action View. See
-
# ActionView::Helpers::CacheHelper for more information.
-
#
-
# While it's strongly recommended that you use key-based cache
-
# expiration (see links in CacheHelper for more information),
-
# it is also possible to manually expire caches. For example:
-
#
-
# expire_fragment('name_of_cache')
-
2
module Fragments
-
# Given a key (as described in +expire_fragment+), returns
-
# a key suitable for use in reading, writing, or expiring a
-
# cached fragment. All keys are prefixed with <tt>views/</tt> and uses
-
# ActiveSupport::Cache.expand_cache_key for the expansion.
-
2
def fragment_cache_key(key)
-
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
-
end
-
-
# Writes +content+ to the location signified by
-
# +key+ (see +expire_fragment+ for acceptable formats).
-
2
def write_fragment(key, content, options = nil)
-
return content unless cache_configured?
-
-
key = fragment_cache_key(key)
-
instrument_fragment_cache :write_fragment, key do
-
content = content.to_str
-
cache_store.write(key, content, options)
-
end
-
content
-
end
-
-
# Reads a cached fragment from the location signified by +key+
-
# (see +expire_fragment+ for acceptable formats).
-
2
def read_fragment(key, options = nil)
-
return unless cache_configured?
-
-
key = fragment_cache_key(key)
-
instrument_fragment_cache :read_fragment, key do
-
result = cache_store.read(key, options)
-
result.respond_to?(:html_safe) ? result.html_safe : result
-
end
-
end
-
-
# Check if a cached fragment from the location signified by
-
# +key+ exists (see +expire_fragment+ for acceptable formats).
-
2
def fragment_exist?(key, options = nil)
-
return unless cache_configured?
-
key = fragment_cache_key(key)
-
-
instrument_fragment_cache :exist_fragment?, key do
-
cache_store.exist?(key, options)
-
end
-
end
-
-
# Removes fragments from the cache.
-
#
-
# +key+ can take one of three forms:
-
#
-
# * String - This would normally take the form of a path, like
-
# <tt>pages/45/notes</tt>.
-
# * Hash - Treated as an implicit call to +url_for+, like
-
# <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
-
# * Regexp - Will remove any fragment that matches, so
-
# <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
-
# don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
-
# the actual filename matched looks like
-
# <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
-
# only supported on caches that can iterate over all keys (unlike
-
# memcached).
-
#
-
# +options+ is passed through to the cache store's +delete+
-
# method (or <tt>delete_matched</tt>, for Regexp keys).
-
2
def expire_fragment(key, options = nil)
-
return unless cache_configured?
-
key = fragment_cache_key(key) unless key.is_a?(Regexp)
-
-
instrument_fragment_cache :expire_fragment, key do
-
if key.is_a?(Regexp)
-
cache_store.delete_matched(key, options)
-
else
-
cache_store.delete(key, options)
-
end
-
end
-
end
-
-
2
def instrument_fragment_cache(name, key) # :nodoc:
-
payload = {
-
controller: controller_name,
-
action: action_name,
-
key: key
-
}
-
-
ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield }
-
end
-
end
-
end
-
end
-
2
module ActionController
-
2
class LogSubscriber < ActiveSupport::LogSubscriber
-
2
INTERNAL_PARAMS = %w(controller action format _method only_path)
-
-
2
def start_processing(event)
-
29
return unless logger.info?
-
-
29
payload = event.payload
-
29
params = payload[:params].except(*INTERNAL_PARAMS)
-
29
format = payload[:format]
-
29
format = format.to_s.upcase if format.is_a?(Symbol)
-
-
29
info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
-
29
info " Parameters: #{params.inspect}" unless params.empty?
-
end
-
-
2
def process_action(event)
-
29
info do
-
29
payload = event.payload
-
29
additions = ActionController::Base.log_process_action(payload)
-
-
29
status = payload[:status]
-
29
if status.nil? && payload[:exception].present?
-
exception_class_name = payload[:exception].first
-
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
-
end
-
29
message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
-
29
message << " (#{additions.join(" | ")})" unless additions.blank?
-
29
message
-
end
-
end
-
-
2
def halted_callback(event)
-
info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
-
end
-
-
2
def send_file(event)
-
info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
-
end
-
-
2
def redirect_to(event)
-
26
info { "Redirected to #{event.payload[:location]}" }
-
end
-
-
2
def send_data(event)
-
info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
-
end
-
-
2
def unpermitted_parameters(event)
-
6
debug do
-
6
unpermitted_keys = event.payload[:keys]
-
6
"Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
-
end
-
end
-
-
2
def deep_munge(event)
-
debug do
-
"Value for params[:#{event.payload[:keys].join('][:')}] was set "\
-
"to nil, because it was one of [], [null] or [null, null, ...]. "\
-
"Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\
-
"for more information."\
-
end
-
end
-
-
%w(write_fragment read_fragment exist_fragment?
-
2
expire_fragment expire_page write_page).each do |method|
-
12
class_eval <<-METHOD, __FILE__, __LINE__ + 1
-
def #{method}(event)
-
return unless logger.info?
-
key_or_path = event.payload[:key] || event.payload[:path]
-
human_name = #{method.to_s.humanize.inspect}
-
info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)")
-
end
-
METHOD
-
end
-
-
2
def logger
-
383
ActionController::Base.logger
-
end
-
end
-
end
-
-
2
ActionController::LogSubscriber.attach_to :action_controller
-
2
require 'active_support/core_ext/array/extract_options'
-
2
require 'action_dispatch/middleware/stack'
-
-
2
module ActionController
-
# Extend ActionDispatch middleware stack to make it aware of options
-
# allowing the following syntax in controllers:
-
#
-
# class PostsController < ApplicationController
-
# use AuthenticationMiddleware, except: [:index, :show]
-
# end
-
#
-
2
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
-
2
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
-
2
def initialize(klass, *args, &block)
-
options = args.extract_options!
-
@only = Array(options.delete(:only)).map(&:to_s)
-
@except = Array(options.delete(:except)).map(&:to_s)
-
args << options unless options.empty?
-
super
-
end
-
-
2
def valid?(action)
-
if @only.present?
-
@only.include?(action)
-
elsif @except.present?
-
!@except.include?(action)
-
else
-
true
-
end
-
end
-
end
-
-
2
def build(action, app = Proc.new)
-
action = action.to_s
-
-
middlewares.reverse.inject(app) do |a, middleware|
-
middleware.valid?(action) ? middleware.build(a) : a
-
end
-
end
-
end
-
-
# <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
-
# valid Rack interface without the additional niceties provided by
-
# <tt>ActionController::Base</tt>.
-
#
-
# A sample metal controller might look like this:
-
#
-
# class HelloController < ActionController::Metal
-
# def index
-
# self.response_body = "Hello World!"
-
# end
-
# end
-
#
-
# And then to route requests to your metal controller, you would add
-
# something like this to <tt>config/routes.rb</tt>:
-
#
-
# get 'hello', to: HelloController.action(:index)
-
#
-
# The +action+ method returns a valid Rack application for the \Rails
-
# router to dispatch to.
-
#
-
# == Rendering Helpers
-
#
-
# <tt>ActionController::Metal</tt> by default provides no utilities for rendering
-
# views, partials, or other responses aside from explicitly calling of
-
# <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To
-
# add the render helpers you're used to having in a normal controller, you
-
# can do the following:
-
#
-
# class HelloController < ActionController::Metal
-
# include AbstractController::Rendering
-
# include ActionView::Layouts
-
# append_view_path "#{Rails.root}/app/views"
-
#
-
# def index
-
# render "hello/index"
-
# end
-
# end
-
#
-
# == Redirection Helpers
-
#
-
# To add redirection helpers to your metal controller, do the following:
-
#
-
# class HelloController < ActionController::Metal
-
# include ActionController::Redirecting
-
# include Rails.application.routes.url_helpers
-
#
-
# def index
-
# redirect_to root_url
-
# end
-
# end
-
#
-
# == Other Helpers
-
#
-
# You can refer to the modules included in <tt>ActionController::Base</tt> to see
-
# other features you can bring into your metal controller.
-
#
-
2
class Metal < AbstractController::Base
-
2
abstract!
-
-
2
attr_internal_writer :env
-
-
2
def env
-
624
@_env ||= {}
-
end
-
-
# Returns the last part of the controller's name, underscored, without the ending
-
# <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
-
# Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
-
#
-
# ==== Returns
-
# * <tt>string</tt>
-
2
def self.controller_name
-
16
@controller_name ||= name.demodulize.sub(/Controller$/, '').underscore
-
end
-
-
# Delegates to the class' <tt>controller_name</tt>
-
2
def controller_name
-
16
self.class.controller_name
-
end
-
-
# The details below can be overridden to support a specific
-
# Request and Response object. The default ActionController::Base
-
# implementation includes RackDelegation, which makes a request
-
# and response object available. You might wish to control the
-
# environment and response manually for performance reasons.
-
-
2
attr_internal :headers, :response, :request
-
2
delegate :session, :to => "@_request"
-
-
2
def initialize
-
29
@_headers = {"Content-Type" => "text/html"}
-
29
@_status = 200
-
29
@_request = nil
-
29
@_response = nil
-
29
@_routes = nil
-
29
super
-
end
-
-
2
def params
-
@_params ||= request.parameters
-
end
-
-
2
def params=(val)
-
@_params = val
-
end
-
-
# Basic implementations for content_type=, location=, and headers are
-
# provided to reduce the dependency on the RackDelegation module
-
# in Renderer and Redirector.
-
-
2
def content_type=(type)
-
headers["Content-Type"] = type.to_s
-
end
-
-
2
def content_type
-
headers["Content-Type"]
-
end
-
-
2
def location
-
headers["Location"]
-
end
-
-
2
def location=(url)
-
headers["Location"] = url
-
end
-
-
# Basic url_for that can be overridden for more robust functionality
-
2
def url_for(string)
-
string
-
end
-
-
2
def status
-
@_status
-
end
-
2
alias :response_code :status # :nodoc:
-
-
2
def status=(status)
-
@_status = Rack::Utils.status_code(status)
-
end
-
-
2
def response_body=(body)
-
29
body = [body] unless body.nil? || body.respond_to?(:each)
-
29
super
-
end
-
-
# Tests if render or redirect has already happened.
-
2
def performed?
-
29
response_body || (response && response.committed?)
-
end
-
-
2
def dispatch(name, request) #:nodoc:
-
@_request = request
-
@_env = request.env
-
@_env['action_controller.instance'] = self
-
process(name)
-
to_a
-
end
-
-
2
def to_a #:nodoc:
-
response ? response.to_a : [status, headers, response_body]
-
end
-
-
2
class_attribute :middleware_stack
-
2
self.middleware_stack = ActionController::MiddlewareStack.new
-
-
2
def self.inherited(base) # :nodoc:
-
12
base.middleware_stack = middleware_stack.dup
-
12
super
-
end
-
-
# Pushes the given Rack middleware and its arguments to the bottom of the
-
# middleware stack.
-
2
def self.use(*args, &block)
-
middleware_stack.use(*args, &block)
-
end
-
-
# Alias for +middleware_stack+.
-
2
def self.middleware
-
middleware_stack
-
end
-
-
# Makes the controller a Rack endpoint that runs the action in the given
-
# +env+'s +action_dispatch.request.path_parameters+ key.
-
2
def self.call(env)
-
req = ActionDispatch::Request.new env
-
action(req.path_parameters[:action]).call(env)
-
end
-
-
# Returns a Rack endpoint for the given action name.
-
2
def self.action(name, klass = ActionDispatch::Request)
-
if middleware_stack.any?
-
middleware_stack.build(name) do |env|
-
new.dispatch(name, klass.new(env))
-
end
-
else
-
lambda { |env| new.dispatch(name, klass.new(env)) }
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/keys'
-
-
2
module ActionController
-
2
module ConditionalGet
-
2
extend ActiveSupport::Concern
-
-
2
include RackDelegation
-
2
include Head
-
-
2
included do
-
2
class_attribute :etaggers
-
2
self.etaggers = []
-
end
-
-
2
module ClassMethods
-
# Allows you to consider additional controller-wide information when generating an ETag.
-
# For example, if you serve pages tailored depending on who's logged in at the moment, you
-
# may want to add the current user id to be part of the ETag to prevent authorized displaying
-
# of cached pages.
-
#
-
# class InvoicesController < ApplicationController
-
# etag { current_user.try :id }
-
#
-
# def show
-
# # Etag will differ even for the same invoice when it's viewed by a different current_user
-
# @invoice = Invoice.find(params[:id])
-
# fresh_when(@invoice)
-
# end
-
# end
-
2
def etag(&etagger)
-
2
self.etaggers += [etagger]
-
end
-
end
-
-
# Sets the +etag+, +last_modified+, or both on the response and renders a
-
# <tt>304 Not Modified</tt> response if the request is already fresh.
-
#
-
# === Parameters:
-
#
-
# * <tt>:etag</tt>.
-
# * <tt>:last_modified</tt>.
-
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
-
# +true+ if you want your application to be cachable by other devices (proxy caches).
-
# * <tt>:template</tt> By default, the template digest for the current
-
# controller/action is included in ETags. If the action renders a
-
# different template, you can include its digest instead. If the action
-
# doesn't render a template at all, you can pass <tt>template: false</tt>
-
# to skip any attempt to check for a template digest.
-
#
-
# === Example:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
# fresh_when(etag: @article, last_modified: @article.created_at, public: true)
-
# end
-
#
-
# This will render the show template if the request isn't sending a matching ETag or
-
# If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
-
#
-
# You can also just pass a record where +last_modified+ will be set by calling
-
# +updated_at+ and the +etag+ by passing the object itself.
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
# fresh_when(@article)
-
# end
-
#
-
# When passing a record, you can still set whether the public header:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
# fresh_when(@article, public: true)
-
# end
-
#
-
# When rendering a different template than the default controller/action
-
# style, you can indicate which digest to include in the ETag:
-
#
-
# before_action { fresh_when @article, template: 'widgets/show' }
-
#
-
2
def fresh_when(record_or_options, additional_options = {})
-
if record_or_options.is_a? Hash
-
options = record_or_options
-
options.assert_valid_keys(:etag, :last_modified, :public, :template)
-
else
-
record = record_or_options
-
options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options)
-
end
-
-
response.etag = combine_etags(options) if options[:etag] || options[:template]
-
response.last_modified = options[:last_modified] if options[:last_modified]
-
response.cache_control[:public] = true if options[:public]
-
-
head :not_modified if request.fresh?(response)
-
end
-
-
# Sets the +etag+ and/or +last_modified+ on the response and checks it against
-
# the client request. If the request doesn't match the options provided, the
-
# request is considered stale and should be generated from scratch. Otherwise,
-
# it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
-
#
-
# === Parameters:
-
#
-
# * <tt>:etag</tt>.
-
# * <tt>:last_modified</tt>.
-
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
-
# +true+ if you want your application to be cachable by other devices (proxy caches).
-
# * <tt>:template</tt> By default, the template digest for the current
-
# controller/action is included in ETags. If the action renders a
-
# different template, you can include its digest instead. If the action
-
# doesn't render a template at all, you can pass <tt>template: false</tt>
-
# to skip any attempt to check for a template digest.
-
#
-
# === Example:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
#
-
# if stale?(etag: @article, last_modified: @article.created_at)
-
# @statistics = @article.really_expensive_call
-
# respond_to do |format|
-
# # all the supported formats
-
# end
-
# end
-
# end
-
#
-
# You can also just pass a record where +last_modified+ will be set by calling
-
# +updated_at+ and the +etag+ by passing the object itself.
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
#
-
# if stale?(@article)
-
# @statistics = @article.really_expensive_call
-
# respond_to do |format|
-
# # all the supported formats
-
# end
-
# end
-
# end
-
#
-
# When passing a record, you can still set whether the public header:
-
#
-
# def show
-
# @article = Article.find(params[:id])
-
#
-
# if stale?(@article, public: true)
-
# @statistics = @article.really_expensive_call
-
# respond_to do |format|
-
# # all the supported formats
-
# end
-
# end
-
# end
-
#
-
# When rendering a different template than the default controller/action
-
# style, you can indicate which digest to include in the ETag:
-
#
-
# def show
-
# super if stale? @article, template: 'widgets/show'
-
# end
-
#
-
2
def stale?(record_or_options, additional_options = {})
-
fresh_when(record_or_options, additional_options)
-
!request.fresh?(response)
-
end
-
-
# Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
-
# instruction, so that intermediate caches must not cache the response.
-
#
-
# expires_in 20.minutes
-
# expires_in 3.hours, public: true
-
# expires_in 3.hours, public: true, must_revalidate: true
-
#
-
# This method will overwrite an existing Cache-Control header.
-
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
-
#
-
# The method will also ensure a HTTP Date header for client compatibility.
-
2
def expires_in(seconds, options = {})
-
response.cache_control.merge!(
-
:max_age => seconds,
-
:public => options.delete(:public),
-
:must_revalidate => options.delete(:must_revalidate)
-
)
-
options.delete(:private)
-
-
response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"}
-
response.date = Time.now unless response.date?
-
end
-
-
# Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
-
# occur by the browser or intermediate caches (like caching proxy servers).
-
2
def expires_now
-
response.cache_control.replace(:no_cache => true)
-
end
-
-
2
private
-
2
def combine_etags(options)
-
etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
-
etags.unshift options[:etag]
-
end
-
end
-
end
-
2
module ActionController #:nodoc:
-
2
module Cookies
-
2
extend ActiveSupport::Concern
-
-
2
include RackDelegation
-
-
2
included do
-
2
helper_method :cookies
-
end
-
-
2
private
-
2
def cookies
-
29
request.cookie_jar
-
end
-
end
-
end
-
2
require 'action_controller/metal/exceptions'
-
-
2
module ActionController #:nodoc:
-
# Methods for sending arbitrary data and for streaming files to the browser,
-
# instead of rendering.
-
2
module DataStreaming
-
2
extend ActiveSupport::Concern
-
-
2
include ActionController::Rendering
-
-
2
DEFAULT_SEND_FILE_TYPE = 'application/octet-stream'.freeze #:nodoc:
-
2
DEFAULT_SEND_FILE_DISPOSITION = 'attachment'.freeze #:nodoc:
-
-
2
protected
-
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
-
# via the Rack::Sendfile middleware. The header to use is set via
-
# +config.action_dispatch.x_sendfile_header+.
-
# Your server can also configure this for you by setting the X-Sendfile-Type header.
-
#
-
# Be careful to sanitize the path parameter if it is coming from a web
-
# page. <tt>send_file(params[:path])</tt> allows a malicious user to
-
# download any file on your server.
-
#
-
# Options:
-
# * <tt>:filename</tt> - suggests a filename for the browser to use.
-
# Defaults to <tt>File.basename(path)</tt>.
-
# * <tt>:type</tt> - specifies an HTTP content type.
-
# You can specify either a string or a symbol for a registered type register with
-
# <tt>Mime::Type.register</tt>, for example :json
-
# If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
-
# If no content type is registered for the extension, default type 'application/octet-stream' will be used.
-
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
-
# Valid values are 'inline' and 'attachment' (default).
-
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
-
# * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from
-
# the URL, which is necessary for i18n filenames on certain browsers
-
# (setting <tt>:filename</tt> overrides this option).
-
#
-
# The default Content-Type and Content-Disposition headers are
-
# set to download arbitrary binary files in as many browsers as
-
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
-
# a variety of quirks (especially when downloading over SSL).
-
#
-
# Simple download:
-
#
-
# send_file '/path/to.zip'
-
#
-
# Show a JPEG in the browser:
-
#
-
# send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline'
-
#
-
# Show a 404 page in the browser:
-
#
-
# send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404
-
#
-
# Read about the other Content-* HTTP headers if you'd like to
-
# provide the user with more information (such as Content-Description) in
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
-
#
-
# Also be aware that the document may be cached by proxies and browsers.
-
# The Pragma and Cache-Control headers declare how the file may be cached
-
# by intermediaries. They default to require clients to validate with
-
# the server before releasing cached responses. See
-
# http://www.mnot.net/cache_docs/ for an overview of web caching and
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
-
# for the Cache-Control header spec.
-
2
def send_file(path, options = {}) #:doc:
-
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
-
-
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
-
send_file_headers! options
-
-
self.status = options[:status] || 200
-
self.content_type = options[:content_type] if options.key?(:content_type)
-
self.response_body = FileBody.new(path)
-
end
-
-
# Avoid having to pass an open file handle as the response body.
-
# Rack::Sendfile will usually intercept the response and uses
-
# the path directly, so there is no reason to open the file.
-
2
class FileBody #:nodoc:
-
2
attr_reader :to_path
-
-
2
def initialize(path)
-
@to_path = path
-
end
-
-
# Stream the file's contents if Rack::Sendfile isn't present.
-
2
def each
-
File.open(to_path, 'rb') do |file|
-
while chunk = file.read(16384)
-
yield chunk
-
end
-
end
-
end
-
end
-
-
# Sends the given binary data to the browser. This method is similar to
-
# <tt>render plain: data</tt>, but also allows you to specify whether
-
# the browser should display the response as a file attachment (i.e. in a
-
# download dialog) or as inline data. You may also set the content type,
-
# the apparent file name, and other things.
-
#
-
# Options:
-
# * <tt>:filename</tt> - suggests a filename for the browser to use.
-
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
-
# either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
-
# If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
-
# If no content type is registered for the extension, default type 'application/octet-stream' will be used.
-
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
-
# Valid values are 'inline' and 'attachment' (default).
-
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
-
#
-
# Generic data download:
-
#
-
# send_data buffer
-
#
-
# Download a dynamically-generated tarball:
-
#
-
# send_data generate_tgz('dir'), filename: 'dir.tgz'
-
#
-
# Display an image Active Record in the browser:
-
#
-
# send_data image.data, type: image.content_type, disposition: 'inline'
-
#
-
# See +send_file+ for more information on HTTP Content-* headers and caching.
-
2
def send_data(data, options = {}) #:doc:
-
send_file_headers! options
-
render options.slice(:status, :content_type).merge(:text => data)
-
end
-
-
2
private
-
2
def send_file_headers!(options)
-
type_provided = options.has_key?(:type)
-
-
content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE)
-
raise ArgumentError, ":type option required" if content_type.nil?
-
-
if content_type.is_a?(Symbol)
-
extension = Mime[content_type]
-
raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension
-
self.content_type = extension
-
else
-
if !type_provided && options[:filename]
-
# If type wasn't provided, try guessing from file extension.
-
content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete('.')) || content_type
-
end
-
self.content_type = content_type
-
end
-
-
disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
-
unless disposition.nil?
-
disposition = disposition.to_s
-
disposition += %(; filename="#{options[:filename]}") if options[:filename]
-
headers['Content-Disposition'] = disposition
-
end
-
-
headers['Content-Transfer-Encoding'] = 'binary'
-
-
response.sending_file = true
-
-
# Fix a problem with IE 6.0 on opening downloaded files:
-
# If Cache-Control: no-cache is set (which Rails does by default),
-
# IE removes the file it just downloaded from its cache immediately
-
# after it displays the "open/save" dialog, which means that if you
-
# hit "open" the file isn't there anymore when the application that
-
# is called for handling the download is run, so let's workaround that
-
response.cache_control[:public] ||= false
-
end
-
end
-
end
-
2
module ActionController
-
# When our views change, they should bubble up into HTTP cache freshness
-
# and bust browser caches. So the template digest for the current action
-
# is automatically included in the ETag.
-
#
-
# Enabled by default for apps that use Action View. Disable by setting
-
#
-
# config.action_controller.etag_with_template_digest = false
-
#
-
# Override the template to digest by passing +:template+ to +fresh_when+
-
# and +stale?+ calls. For example:
-
#
-
# # We're going to render widgets/show, not posts/show
-
# fresh_when @post, template: 'widgets/show'
-
#
-
# # We're not going to render a template, so omit it from the ETag.
-
# fresh_when @post, template: false
-
#
-
2
module EtagWithTemplateDigest
-
2
extend ActiveSupport::Concern
-
-
2
include ActionController::ConditionalGet
-
-
2
included do
-
2
class_attribute :etag_with_template_digest
-
2
self.etag_with_template_digest = true
-
-
2
ActiveSupport.on_load :action_view, yield: true do |action_view_base|
-
2
etag do |options|
-
determine_template_etag(options) if etag_with_template_digest
-
end
-
end
-
end
-
-
2
private
-
2
def determine_template_etag(options)
-
if template = pick_template_for_etag(options)
-
lookup_and_digest_template(template)
-
end
-
end
-
-
2
def pick_template_for_etag(options)
-
options.fetch(:template) { "#{controller_name}/#{action_name}" }
-
end
-
-
2
def lookup_and_digest_template(template)
-
ActionView::Digestor.digest name: template, finder: lookup_context
-
end
-
end
-
end
-
2
module ActionController #:nodoc:
-
2
module Flash
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :_flash_types, instance_accessor: false
-
2
self._flash_types = []
-
-
2
delegate :flash, to: :request
-
2
add_flash_types(:alert, :notice)
-
end
-
-
2
module ClassMethods
-
# Creates new flash types. You can pass as many types as you want to create
-
# flash types other than the default <tt>alert</tt> and <tt>notice</tt> in
-
# your controllers and views. For instance:
-
#
-
# # in application_controller.rb
-
# class ApplicationController < ActionController::Base
-
# add_flash_types :warning
-
# end
-
#
-
# # in your controller
-
# redirect_to user_path(@user), warning: "Incomplete profile"
-
#
-
# # in your view
-
# <%= warning %>
-
#
-
# This method will automatically define a new method for each of the given
-
# names, and it will be available in your views.
-
2
def add_flash_types(*types)
-
2
types.each do |type|
-
4
next if _flash_types.include?(type)
-
-
4
define_method(type) do
-
33
request.flash[type]
-
end
-
4
helper_method type
-
-
4
self._flash_types += [type]
-
end
-
end
-
end
-
-
2
protected
-
2
def redirect_to(options = {}, response_status_and_flash = {}) #:doc:
-
13
self.class._flash_types.each do |flash_type|
-
26
if type = response_status_and_flash.delete(flash_type)
-
13
flash[flash_type] = type
-
end
-
end
-
-
13
if other_flashes = response_status_and_flash.delete(:flash)
-
flash.update(other_flashes)
-
end
-
-
13
super(options, response_status_and_flash)
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/hash/slice'
-
-
2
module ActionController
-
# This module provides a method which will redirect browser to use HTTPS
-
# protocol. This will ensure that user's sensitive information will be
-
# transferred safely over the internet. You _should_ always force browser
-
# to use HTTPS when you're transferring sensitive information such as
-
# user authentication, account information, or credit card information.
-
#
-
# Note that if you are really concerned about your application security,
-
# you might consider using +config.force_ssl+ in your config file instead.
-
# That will ensure all the data transferred via HTTPS protocol and prevent
-
# user from getting session hijacked when accessing the site under unsecured
-
# HTTP protocol.
-
2
module ForceSSL
-
2
extend ActiveSupport::Concern
-
2
include AbstractController::Callbacks
-
-
2
ACTION_OPTIONS = [:only, :except, :if, :unless]
-
2
URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path]
-
2
REDIRECT_OPTIONS = [:status, :flash, :alert, :notice]
-
-
2
module ClassMethods
-
# Force the request to this particular controller or specified actions to be
-
# under HTTPS protocol.
-
#
-
# If you need to disable this for any reason (e.g. development) then you can use
-
# an +:if+ or +:unless+ condition.
-
#
-
# class AccountsController < ApplicationController
-
# force_ssl if: :ssl_configured?
-
#
-
# def ssl_configured?
-
# !Rails.env.development?
-
# end
-
# end
-
#
-
# ==== URL Options
-
# You can pass any of the following options to affect the redirect url
-
# * <tt>host</tt> - Redirect to a different host name
-
# * <tt>subdomain</tt> - Redirect to a different subdomain
-
# * <tt>domain</tt> - Redirect to a different domain
-
# * <tt>port</tt> - Redirect to a non-standard port
-
# * <tt>path</tt> - Redirect to a different path
-
#
-
# ==== Redirect Options
-
# You can pass any of the following options to affect the redirect status and response
-
# * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently)
-
# * <tt>flash</tt> - Set a flash message when redirecting
-
# * <tt>alert</tt> - Set an alert message when redirecting
-
# * <tt>notice</tt> - Set a notice message when redirecting
-
#
-
# ==== Action Options
-
# You can pass any of the following options to affect the before_action callback
-
# * <tt>only</tt> - The callback should be run only for this action
-
# * <tt>except</tt> - The callback should be run for all actions except this action
-
# * <tt>if</tt> - A symbol naming an instance method or a proc; the callback
-
# will be called only when it returns a true value.
-
# * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback
-
# will be called only when it returns a false value.
-
2
def force_ssl(options = {})
-
action_options = options.slice(*ACTION_OPTIONS)
-
redirect_options = options.except(*ACTION_OPTIONS)
-
before_action(action_options) do
-
force_ssl_redirect(redirect_options)
-
end
-
end
-
end
-
-
# Redirect the existing request to use the HTTPS protocol.
-
#
-
# ==== Parameters
-
# * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options
-
# available to the <tt>force_ssl</tt> method.
-
2
def force_ssl_redirect(host_or_options = nil)
-
unless request.ssl?
-
options = {
-
:protocol => 'https://',
-
:host => request.host,
-
:path => request.fullpath,
-
:status => :moved_permanently
-
}
-
-
if host_or_options.is_a?(Hash)
-
options.merge!(host_or_options)
-
elsif host_or_options
-
options[:host] = host_or_options
-
end
-
-
secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS))
-
flash.keep if respond_to?(:flash)
-
redirect_to secure_url, options.slice(*REDIRECT_OPTIONS)
-
end
-
end
-
end
-
end
-
2
module ActionController
-
2
module Head
-
# Returns a response that has no content (merely headers). The options
-
# argument is interpreted to be a hash of header names and values.
-
# This allows you to easily return a response that consists only of
-
# significant headers:
-
#
-
# head :created, location: person_path(@person)
-
#
-
# head :created, location: @person
-
#
-
# It can also be used to return exceptional conditions:
-
#
-
# return head(:method_not_allowed) unless request.post?
-
# return head(:bad_request) unless valid_request?
-
# render
-
#
-
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
-
2
def head(status, options = {})
-
options, status = status, nil if status.is_a?(Hash)
-
status ||= options.delete(:status) || :ok
-
location = options.delete(:location)
-
content_type = options.delete(:content_type)
-
-
options.each do |key, value|
-
headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s
-
end
-
-
self.status = status
-
self.location = url_for(location) if location
-
-
self.response_body = ""
-
-
if include_content?(self.response_code)
-
self.content_type = content_type || (Mime[formats.first] if formats)
-
self.response.charset = false if self.response
-
else
-
headers.delete('Content-Type')
-
headers.delete('Content-Length')
-
end
-
-
true
-
end
-
-
2
private
-
# :nodoc:
-
2
def include_content?(status)
-
case status
-
when 100..199
-
false
-
when 204, 205, 304
-
false
-
else
-
true
-
end
-
end
-
end
-
end
-
2
module ActionController
-
# The \Rails framework provides a large number of helpers for working with assets, dates, forms,
-
# numbers and model objects, to name a few. These helpers are available to all templates
-
# by default.
-
#
-
# In addition to using the standard template helpers provided, creating custom helpers to
-
# extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
-
# will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt>
-
#
-
# In previous versions of \Rails the controller will include a helper whose
-
# name matches that of the controller, e.g., <tt>MyController</tt> will automatically
-
# include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+.
-
#
-
# Additional helpers can be specified using the +helper+ class method in ActionController::Base or any
-
# controller which inherits from it.
-
#
-
# The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if
-
# a \Time object is blank:
-
#
-
# module FormattedTimeHelper
-
# def format_time(time, format=:long, blank_message=" ")
-
# time.blank? ? blank_message : time.to_s(format)
-
# end
-
# end
-
#
-
# FormattedTimeHelper can now be included in a controller, using the +helper+ class method:
-
#
-
# class EventsController < ActionController::Base
-
# helper FormattedTimeHelper
-
# def index
-
# @events = Event.all
-
# end
-
# end
-
#
-
# Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
-
#
-
# <% @events.each do |event| -%>
-
# <p>
-
# <%= format_time(event.time, :short, "N/A") %> | <%= event.name %>
-
# </p>
-
# <% end -%>
-
#
-
# Finally, assuming we have two event instances, one which has a time and one which does not,
-
# the output might look like this:
-
#
-
# 23 Aug 11:30 | Carolina Railhawks Soccer Match
-
# N/A | Carolina Railhaws Training Workshop
-
#
-
2
module Helpers
-
2
extend ActiveSupport::Concern
-
-
4
class << self; attr_accessor :helpers_path; end
-
2
include AbstractController::Helpers
-
-
2
included do
-
2
class_attribute :helpers_path, :include_all_helpers
-
2
self.helpers_path ||= []
-
2
self.include_all_helpers = true
-
end
-
-
2
module ClassMethods
-
# Declares helper accessors for controller attributes. For example, the
-
# following adds new +name+ and <tt>name=</tt> instance methods to a
-
# controller and makes them available to the view:
-
# attr_accessor :name
-
# helper_attr :name
-
#
-
# ==== Parameters
-
# * <tt>attrs</tt> - Names of attributes to be converted into helpers.
-
2
def helper_attr(*attrs)
-
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
-
end
-
-
# Provides a proxy to access helpers methods from outside the view.
-
2
def helpers
-
@helper_proxy ||= begin
-
proxy = ActionView::Base.new
-
proxy.config = config.inheritable_copy
-
proxy.extend(_helpers)
-
end
-
end
-
-
# Overwrite modules_for_helpers to accept :all as argument, which loads
-
# all helpers in helpers_path.
-
#
-
# ==== Parameters
-
# * <tt>args</tt> - A list of helpers
-
#
-
# ==== Returns
-
# * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
-
2
def modules_for_helpers(args)
-
11
args += all_application_helpers if args.delete(:all)
-
11
super(args)
-
end
-
-
2
def all_helpers_from_path(path)
-
2
helpers = Array(path).flat_map do |_path|
-
4
extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
-
16
names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') }
-
4
names.sort!
-
end
-
2
helpers.uniq!
-
2
helpers
-
end
-
-
2
private
-
# Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
-
2
def all_application_helpers
-
2
all_helpers_from_path(helpers_path)
-
end
-
end
-
end
-
end
-
-
2
module ActionController
-
# Adds the ability to prevent public methods on a controller to be called as actions.
-
2
module HideActions
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :hidden_actions
-
2
self.hidden_actions = Set.new.freeze
-
end
-
-
2
private
-
-
# Overrides AbstractController::Base#action_method? to return false if the
-
# action name is in the list of hidden actions.
-
2
def method_for_action(action_name)
-
29
self.class.visible_action?(action_name) && super
-
end
-
-
2
module ClassMethods
-
# Sets all of the actions passed in as hidden actions.
-
#
-
# ==== Parameters
-
# * <tt>args</tt> - A list of actions
-
2
def hide_action(*args)
-
self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze
-
end
-
-
2
def visible_action?(action_name)
-
29
not hidden_actions.include?(action_name)
-
end
-
-
# Overrides AbstractController::Base#action_methods to remove any methods
-
# that are listed as hidden methods.
-
2
def action_methods
-
198
@action_methods ||= Set.new(super.reject { |name| hidden_actions.include?(name) }).freeze
-
end
-
end
-
end
-
end
-
2
require 'base64'
-
2
require 'active_support/security_utils'
-
-
2
module ActionController
-
# Makes it dead easy to do HTTP Basic, Digest and Token authentication.
-
2
module HttpAuthentication
-
# Makes it dead easy to do HTTP \Basic authentication.
-
#
-
# === Simple \Basic example
-
#
-
# class PostsController < ApplicationController
-
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index
-
#
-
# def index
-
# render plain: "Everyone can see me!"
-
# end
-
#
-
# def edit
-
# render plain: "I'm only accessible if you know the password"
-
# end
-
# end
-
#
-
# === Advanced \Basic example
-
#
-
# Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
-
# the regular HTML interface is protected by a session approach:
-
#
-
# class ApplicationController < ActionController::Base
-
# before_action :set_account, :authenticate
-
#
-
# protected
-
# def set_account
-
# @account = Account.find_by(url_name: request.subdomains.first)
-
# end
-
#
-
# def authenticate
-
# case request.format
-
# when Mime::XML, Mime::ATOM
-
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
-
# @current_user = user
-
# else
-
# request_http_basic_authentication
-
# end
-
# else
-
# if session_authenticated?
-
# @current_user = @account.users.find(session[:authenticated][:user_id])
-
# else
-
# redirect_to(login_url) and return false
-
# end
-
# end
-
# end
-
# end
-
#
-
# In your integration tests, you can do something like this:
-
#
-
# def test_access_granted_from_xml
-
# @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
-
# get "/notes/1.xml"
-
#
-
# assert_equal 200, status
-
# end
-
2
module Basic
-
2
extend self
-
-
2
module ControllerMethods
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
2
def http_basic_authenticate_with(options = {})
-
before_action(options.except(:name, :password, :realm)) do
-
authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
-
# This comparison uses & so that it doesn't short circuit and
-
# uses `variable_size_secure_compare` so that length information
-
# isn't leaked.
-
ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) &
-
ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password])
-
end
-
end
-
end
-
end
-
-
2
def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure)
-
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm)
-
end
-
-
2
def authenticate_with_http_basic(&login_procedure)
-
HttpAuthentication::Basic.authenticate(request, &login_procedure)
-
end
-
-
2
def request_http_basic_authentication(realm = "Application")
-
HttpAuthentication::Basic.authentication_request(self, realm)
-
end
-
end
-
-
2
def authenticate(request, &login_procedure)
-
if has_basic_credentials?(request)
-
login_procedure.call(*user_name_and_password(request))
-
end
-
end
-
-
2
def has_basic_credentials?(request)
-
request.authorization.present? && (auth_scheme(request) == 'Basic')
-
end
-
-
2
def user_name_and_password(request)
-
decode_credentials(request).split(':', 2)
-
end
-
-
2
def decode_credentials(request)
-
::Base64.decode64(auth_param(request) || '')
-
end
-
-
2
def auth_scheme(request)
-
request.authorization.split(' ', 2).first
-
end
-
-
2
def auth_param(request)
-
request.authorization.split(' ', 2).second
-
end
-
-
2
def encode_credentials(user_name, password)
-
"Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
-
end
-
-
2
def authentication_request(controller, realm)
-
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
-
controller.status = 401
-
controller.response_body = "HTTP Basic: Access denied.\n"
-
end
-
end
-
-
# Makes it dead easy to do HTTP \Digest authentication.
-
#
-
# === Simple \Digest example
-
#
-
# require 'digest/md5'
-
# class PostsController < ApplicationController
-
# REALM = "SuperSecret"
-
# USERS = {"dhh" => "secret", #plain text password
-
# "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
-
#
-
# before_action :authenticate, except: [:index]
-
#
-
# def index
-
# render plain: "Everyone can see me!"
-
# end
-
#
-
# def edit
-
# render plain: "I'm only accessible if you know the password"
-
# end
-
#
-
# private
-
# def authenticate
-
# authenticate_or_request_with_http_digest(REALM) do |username|
-
# USERS[username]
-
# end
-
# end
-
# end
-
#
-
# === Notes
-
#
-
# The +authenticate_or_request_with_http_digest+ block must return the user's password
-
# or the ha1 digest hash so the framework can appropriately hash to check the user's
-
# credentials. Returning +nil+ will cause authentication to fail.
-
#
-
# Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
-
# the password file or database is compromised, the attacker would be able to use the ha1 hash to
-
# authenticate as the user at this +realm+, but would not have the user's password to try using at
-
# other sites.
-
#
-
# In rare instances, web servers or front proxies strip authorization headers before
-
# they reach your application. You can debug this situation by logging all environment
-
# variables, and check for HTTP_AUTHORIZATION, amongst others.
-
2
module Digest
-
2
extend self
-
-
2
module ControllerMethods
-
2
def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
-
authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm)
-
end
-
-
# Authenticate with HTTP Digest, returns true or false
-
2
def authenticate_with_http_digest(realm = "Application", &password_procedure)
-
HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
-
end
-
-
# Render output including the HTTP Digest authentication header
-
2
def request_http_digest_authentication(realm = "Application", message = nil)
-
HttpAuthentication::Digest.authentication_request(self, realm, message)
-
end
-
end
-
-
# Returns false on a valid response, true otherwise
-
2
def authenticate(request, realm, &password_procedure)
-
request.authorization && validate_digest_response(request, realm, &password_procedure)
-
end
-
-
# Returns false unless the request credentials response value matches the expected value.
-
# First try the password as a ha1 digest password. If this fails, then try it as a plain
-
# text password.
-
2
def validate_digest_response(request, realm, &password_procedure)
-
secret_key = secret_token(request)
-
credentials = decode_credentials_header(request)
-
valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
-
-
if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
-
password = password_procedure.call(credentials[:username])
-
return false unless password
-
-
method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
-
uri = credentials[:uri]
-
-
[true, false].any? do |trailing_question_mark|
-
[true, false].any? do |password_is_ha1|
-
_uri = trailing_question_mark ? uri + "?" : uri
-
expected = expected_response(method, _uri, credentials, password, password_is_ha1)
-
expected == credentials[:response]
-
end
-
end
-
end
-
end
-
-
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
-
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
-
# of a plain-text password.
-
2
def expected_response(http_method, uri, credentials, password, password_is_ha1=true)
-
ha1 = password_is_ha1 ? password : ha1(credentials, password)
-
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':'))
-
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':'))
-
end
-
-
2
def ha1(credentials, password)
-
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
-
end
-
-
2
def encode_credentials(http_method, credentials, password, password_is_ha1)
-
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
-
"Digest " + credentials.sort_by {|x| x[0].to_s }.map {|v| "#{v[0]}='#{v[1]}'" }.join(', ')
-
end
-
-
2
def decode_credentials_header(request)
-
decode_credentials(request.authorization)
-
end
-
-
2
def decode_credentials(header)
-
ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, '').split(',').map do |pair|
-
key, value = pair.split('=', 2)
-
[key.strip, value.to_s.gsub(/^"|"$/,'').delete('\'')]
-
end]
-
end
-
-
2
def authentication_header(controller, realm)
-
secret_key = secret_token(controller.request)
-
nonce = self.nonce(secret_key)
-
opaque = opaque(secret_key)
-
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
-
end
-
-
2
def authentication_request(controller, realm, message = nil)
-
message ||= "HTTP Digest: Access denied.\n"
-
authentication_header(controller, realm)
-
controller.status = 401
-
controller.response_body = message
-
end
-
-
2
def secret_token(request)
-
key_generator = request.env["action_dispatch.key_generator"]
-
http_auth_salt = request.env["action_dispatch.http_auth_salt"]
-
key_generator.generate_key(http_auth_salt)
-
end
-
-
# Uses an MD5 digest based on time to generate a value to be used only once.
-
#
-
# A server-specified data string which should be uniquely generated each time a 401 response is made.
-
# It is recommended that this string be base64 or hexadecimal data.
-
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
-
#
-
# The contents of the nonce are implementation dependent.
-
# The quality of the implementation depends on a good choice.
-
# A nonce might, for example, be constructed as the base 64 encoding of
-
#
-
# time-stamp H(time-stamp ":" ETag ":" private-key)
-
#
-
# where time-stamp is a server-generated time or other non-repeating value,
-
# ETag is the value of the HTTP ETag header associated with the requested entity,
-
# and private-key is data known only to the server.
-
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
-
# reject the request if it did not match the nonce from that header or
-
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
-
# The inclusion of the ETag prevents a replay request for an updated version of the resource.
-
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability
-
# to limit the reuse of the nonce to the same client that originally got it.
-
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
-
# Also, IP address spoofing is not that hard.)
-
#
-
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
-
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
-
# POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
-
# of this document.
-
#
-
# The nonce is opaque to the client. Composed of Time, and hash of Time with secret
-
# key from the Rails session secret generated upon creation of project. Ensures
-
# the time cannot be modified by client.
-
2
def nonce(secret_key, time = Time.now)
-
t = time.to_i
-
hashed = [t, secret_key]
-
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
-
::Base64.strict_encode64("#{t}:#{digest}")
-
end
-
-
# Might want a shorter timeout depending on whether the request
-
# is a PATCH, PUT, or POST, and if client is browser or web service.
-
# Can be much shorter if the Stale directive is implemented. This would
-
# allow a user to use new nonce without prompting user again for their
-
# username and password.
-
2
def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60)
-
return false if value.nil?
-
t = ::Base64.decode64(value).split(":").first.to_i
-
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
-
end
-
-
# Opaque based on random generation - but changing each request?
-
2
def opaque(secret_key)
-
::Digest::MD5.hexdigest(secret_key)
-
end
-
-
end
-
-
# Makes it dead easy to do HTTP Token authentication.
-
#
-
# Simple Token example:
-
#
-
# class PostsController < ApplicationController
-
# TOKEN = "secret"
-
#
-
# before_action :authenticate, except: [ :index ]
-
#
-
# def index
-
# render plain: "Everyone can see me!"
-
# end
-
#
-
# def edit
-
# render plain: "I'm only accessible if you know the password"
-
# end
-
#
-
# private
-
# def authenticate
-
# authenticate_or_request_with_http_token do |token, options|
-
# token == TOKEN
-
# end
-
# end
-
# end
-
#
-
#
-
# Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
-
# the regular HTML interface is protected by a session approach:
-
#
-
# class ApplicationController < ActionController::Base
-
# before_action :set_account, :authenticate
-
#
-
# protected
-
# def set_account
-
# @account = Account.find_by(url_name: request.subdomains.first)
-
# end
-
#
-
# def authenticate
-
# case request.format
-
# when Mime::XML, Mime::ATOM
-
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
-
# @current_user = user
-
# else
-
# request_http_token_authentication
-
# end
-
# else
-
# if session_authenticated?
-
# @current_user = @account.users.find(session[:authenticated][:user_id])
-
# else
-
# redirect_to(login_url) and return false
-
# end
-
# end
-
# end
-
# end
-
#
-
#
-
# In your integration tests, you can do something like this:
-
#
-
# def test_access_granted_from_xml
-
# get(
-
# "/notes/1.xml", nil,
-
# 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
-
# )
-
#
-
# assert_equal 200, status
-
# end
-
#
-
#
-
# On shared hosts, Apache sometimes doesn't pass authentication headers to
-
# FCGI instances. If your environment matches this description and you cannot
-
# authenticate, try this rule in your Apache setup:
-
#
-
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
-
2
module Token
-
2
TOKEN_KEY = 'token='
-
2
TOKEN_REGEX = /^Token /
-
2
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
-
2
extend self
-
-
2
module ControllerMethods
-
2
def authenticate_or_request_with_http_token(realm = "Application", &login_procedure)
-
authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm)
-
end
-
-
2
def authenticate_with_http_token(&login_procedure)
-
Token.authenticate(self, &login_procedure)
-
end
-
-
2
def request_http_token_authentication(realm = "Application")
-
Token.authentication_request(self, realm)
-
end
-
end
-
-
# If token Authorization header is present, call the login
-
# procedure with the present token and options.
-
#
-
# [controller]
-
# ActionController::Base instance for the current request.
-
#
-
# [login_procedure]
-
# Proc to call if a token is present. The Proc should take two arguments:
-
#
-
# authenticate(controller) { |token, options| ... }
-
#
-
# Returns the return value of <tt>login_procedure</tt> if a
-
# token is found. Returns <tt>nil</tt> if no token is found.
-
-
2
def authenticate(controller, &login_procedure)
-
token, options = token_and_options(controller.request)
-
unless token.blank?
-
login_procedure.call(token, options)
-
end
-
end
-
-
# Parses the token and options out of the token authorization header. If
-
# the header looks like this:
-
# Authorization: Token token="abc", nonce="def"
-
# Then the returned token is "abc", and the options is {nonce: "def"}
-
#
-
# request - ActionDispatch::Request instance with the current headers.
-
#
-
# Returns an Array of [String, Hash] if a token is present.
-
# Returns nil if no token is found.
-
2
def token_and_options(request)
-
authorization_request = request.authorization.to_s
-
if authorization_request[TOKEN_REGEX]
-
params = token_params_from authorization_request
-
[params.shift[1], Hash[params].with_indifferent_access]
-
end
-
end
-
-
2
def token_params_from(auth)
-
rewrite_param_values params_array_from raw_params auth
-
end
-
-
# Takes raw_params and turns it into an array of parameters
-
2
def params_array_from(raw_params)
-
raw_params.map { |param| param.split %r/=(.+)?/ }
-
end
-
-
# This removes the <tt>"</tt> characters wrapping the value.
-
2
def rewrite_param_values(array_params)
-
array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' }
-
end
-
-
# This method takes an authorization body and splits up the key-value
-
# pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
-
# delimiters defined in +AUTHN_PAIR_DELIMITERS+.
-
2
def raw_params(auth)
-
_raw_params = auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
-
-
if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}})
-
_raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
-
end
-
-
_raw_params
-
end
-
-
# Encodes the given token and options into an Authorization header value.
-
#
-
# token - String token.
-
# options - optional Hash of the options.
-
#
-
# Returns String.
-
2
def encode_credentials(token, options = {})
-
values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
-
"#{key}=#{value.to_s.inspect}"
-
end
-
"Token #{values * ", "}"
-
end
-
-
# Sets a WWW-Authenticate to let the client know a token is desired.
-
#
-
# controller - ActionController::Base instance for the outgoing response.
-
# realm - String realm to use in the header.
-
#
-
# Returns nothing.
-
2
def authentication_request(controller, realm)
-
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
-
controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized
-
end
-
end
-
end
-
end
-
2
module ActionController
-
2
module ImplicitRender
-
2
def send_action(method, *args)
-
29
ret = super
-
29
default_render unless performed?
-
29
ret
-
end
-
-
2
def default_render(*args)
-
12
render(*args)
-
end
-
-
2
def method_for_action(action_name)
-
super || if template_exists?(action_name.to_s, _prefixes)
-
"default_render"
-
29
end
-
end
-
end
-
end
-
2
require 'benchmark'
-
2
require 'abstract_controller/logger'
-
-
2
module ActionController
-
# Adds instrumentation to several ends in ActionController::Base. It also provides
-
# some hooks related with process_action, this allows an ORM like Active Record
-
# and/or DataMapper to plug in ActionController and show related information.
-
#
-
# Check ActiveRecord::Railties::ControllerRuntime for an example.
-
2
module Instrumentation
-
2
extend ActiveSupport::Concern
-
-
2
include AbstractController::Logger
-
2
include ActionController::RackDelegation
-
-
2
attr_internal :view_runtime
-
-
2
def process_action(*args)
-
29
raw_payload = {
-
:controller => self.class.name,
-
:action => self.action_name,
-
:params => request.filtered_parameters,
-
:format => request.format.try(:ref),
-
:method => request.request_method,
-
29
:path => (request.fullpath rescue "unknown")
-
}
-
-
29
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
-
-
29
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
-
29
begin
-
29
result = super
-
29
payload[:status] = response.status
-
29
result
-
ensure
-
29
append_info_to_payload(payload)
-
end
-
end
-
end
-
-
2
def render(*args)
-
16
render_output = nil
-
16
self.view_runtime = cleanup_view_runtime do
-
32
Benchmark.ms { render_output = super }
-
end
-
16
render_output
-
end
-
-
2
def send_file(path, options={})
-
ActiveSupport::Notifications.instrument("send_file.action_controller",
-
options.merge(:path => path)) do
-
super
-
end
-
end
-
-
2
def send_data(data, options = {})
-
ActiveSupport::Notifications.instrument("send_data.action_controller", options) do
-
super
-
end
-
end
-
-
2
def redirect_to(*args)
-
13
ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload|
-
13
result = super
-
13
payload[:status] = response.status
-
13
payload[:location] = response.filtered_location
-
13
result
-
end
-
end
-
-
2
private
-
-
# A hook invoked every time a before callback is halted.
-
2
def halted_callback_hook(filter)
-
ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter)
-
end
-
-
# A hook which allows you to clean up any time taken into account in
-
# views wrongly, like database querying time.
-
#
-
# def cleanup_view_runtime
-
# super - time_taken_in_something_expensive
-
# end
-
#
-
# :api: plugin
-
2
def cleanup_view_runtime #:nodoc:
-
16
yield
-
end
-
-
# Every time after an action is processed, this method is invoked
-
# with the payload, so you can add more information.
-
# :api: plugin
-
2
def append_info_to_payload(payload) #:nodoc:
-
29
payload[:view_runtime] = view_runtime
-
end
-
-
2
module ClassMethods
-
# A hook which allows other frameworks to log what happened during
-
# controller process action. This method should return an array
-
# with the messages to be added.
-
# :api: plugin
-
2
def log_process_action(payload) #:nodoc:
-
29
messages, view_runtime = [], payload[:view_runtime]
-
29
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
-
29
messages
-
end
-
end
-
end
-
end
-
2
require 'abstract_controller/collector'
-
-
2
module ActionController #:nodoc:
-
2
module MimeResponds
-
2
extend ActiveSupport::Concern
-
-
# :stopdoc:
-
2
module ClassMethods
-
2
def respond_to(*)
-
raise NoMethodError, "The controller-level `respond_to' feature has " \
-
"been extracted to the `responders` gem. Add it to your Gemfile to " \
-
"continue using this feature:\n" \
-
" gem 'responders', '~> 2.0'\n" \
-
"Consult the Rails upgrade guide for details."
-
end
-
end
-
-
2
def respond_with(*)
-
raise NoMethodError, "The `respond_with' feature has been extracted " \
-
"to the `responders` gem. Add it to your Gemfile to continue using " \
-
"this feature:\n" \
-
" gem 'responders', '~> 2.0'\n" \
-
"Consult the Rails upgrade guide for details."
-
end
-
# :startdoc:
-
-
# Without web-service support, an action which collects the data for displaying a list of people
-
# might look something like this:
-
#
-
# def index
-
# @people = Person.all
-
# end
-
#
-
# Here's the same action, with web-service support baked in:
-
#
-
# def index
-
# @people = Person.all
-
#
-
# respond_to do |format|
-
# format.html
-
# format.xml { render xml: @people }
-
# end
-
# end
-
#
-
# What that says is, "if the client wants HTML in response to this action, just respond as we
-
# would have before, but if the client wants XML, return them the list of people in XML format."
-
# (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
-
#
-
# Supposing you have an action that adds a new person, optionally creating their company
-
# (by name) if it does not already exist, without web-services, it might look like this:
-
#
-
# def create
-
# @company = Company.find_or_create_by(name: params[:company][:name])
-
# @person = @company.people.create(params[:person])
-
#
-
# redirect_to(person_list_url)
-
# end
-
#
-
# Here's the same action, with web-service support baked in:
-
#
-
# def create
-
# company = params[:person].delete(:company)
-
# @company = Company.find_or_create_by(name: company[:name])
-
# @person = @company.people.create(params[:person])
-
#
-
# respond_to do |format|
-
# format.html { redirect_to(person_list_url) }
-
# format.js
-
# format.xml { render xml: @person.to_xml(include: @company) }
-
# end
-
# end
-
#
-
# If the client wants HTML, we just redirect them back to the person list. If they want JavaScript,
-
# then it is an Ajax request and we render the JavaScript template associated with this action.
-
# Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
-
# include the person's company in the rendered XML, so you get something like this:
-
#
-
# <person>
-
# <id>...</id>
-
# ...
-
# <company>
-
# <id>...</id>
-
# <name>...</name>
-
# ...
-
# </company>
-
# </person>
-
#
-
# Note, however, the extra bit at the top of that action:
-
#
-
# company = params[:person].delete(:company)
-
# @company = Company.find_or_create_by(name: company[:name])
-
#
-
# This is because the incoming XML document (if a web-service request is in process) can only contain a
-
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
-
#
-
# person[name]=...&person[company][name]=...&...
-
#
-
# And, like this (xml-encoded):
-
#
-
# <person>
-
# <name>...</name>
-
# <company>
-
# <name>...</name>
-
# </company>
-
# </person>
-
#
-
# In other words, we make the request so that it operates on a single entity's person. Then, in the action,
-
# we extract the company data from the request, find or create the company, and then create the new person
-
# with the remaining data.
-
#
-
# Note that you can define your own XML parameter parser which would allow you to describe multiple entities
-
# in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
-
# and accept Rails' defaults, life will be much easier.
-
#
-
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
-
# config/initializers/mime_types.rb as follows.
-
#
-
# Mime::Type.register "image/jpg", :jpg
-
#
-
# Respond to also allows you to specify a common block for different formats by using any:
-
#
-
# def index
-
# @people = Person.all
-
#
-
# respond_to do |format|
-
# format.html
-
# format.any(:xml, :json) { render request.format.to_sym => @people }
-
# end
-
# end
-
#
-
# In the example above, if the format is xml, it will render:
-
#
-
# render xml: @people
-
#
-
# Or if the format is json:
-
#
-
# render json: @people
-
#
-
# Formats can have different variants.
-
#
-
# The request variant is a specialization of the request format, like <tt>:tablet</tt>,
-
# <tt>:phone</tt>, or <tt>:desktop</tt>.
-
#
-
# We often want to render different html/json/xml templates for phones,
-
# tablets, and desktop browsers. Variants make it easy.
-
#
-
# You can set the variant in a +before_action+:
-
#
-
# request.variant = :tablet if request.user_agent =~ /iPad/
-
#
-
# Respond to variants in the action just like you respond to formats:
-
#
-
# respond_to do |format|
-
# format.html do |variant|
-
# variant.tablet # renders app/views/projects/show.html+tablet.erb
-
# variant.phone { extra_setup; render ... }
-
# variant.none { special_setup } # executed only if there is no variant set
-
# end
-
# end
-
#
-
# Provide separate templates for each format and variant:
-
#
-
# app/views/projects/show.html.erb
-
# app/views/projects/show.html+tablet.erb
-
# app/views/projects/show.html+phone.erb
-
#
-
# When you're not sharing any code within the format, you can simplify defining variants
-
# using the inline syntax:
-
#
-
# respond_to do |format|
-
# format.js { render "trash" }
-
# format.html.phone { redirect_to progress_path }
-
# format.html.none { render "trash" }
-
# end
-
#
-
# Variants also support common `any`/`all` block that formats have.
-
#
-
# It works for both inline:
-
#
-
# respond_to do |format|
-
# format.html.any { render text: "any" }
-
# format.html.phone { render text: "phone" }
-
# end
-
#
-
# and block syntax:
-
#
-
# respond_to do |format|
-
# format.html do |variant|
-
# variant.any(:tablet, :phablet){ render text: "any" }
-
# variant.phone { render text: "phone" }
-
# end
-
# end
-
#
-
# You can also set an array of variants:
-
#
-
# request.variant = [:tablet, :phone]
-
#
-
# which will work similarly to formats and MIME types negotiation. If there will be no
-
# :tablet variant declared, :phone variant will be picked:
-
#
-
# respond_to do |format|
-
# format.html.none
-
# format.html.phone # this gets rendered
-
# end
-
#
-
# Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt>
-
# for more examples.
-
2
def respond_to(*mimes)
-
17
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
-
-
17
collector = Collector.new(mimes, request.variant)
-
17
yield collector if block_given?
-
-
17
if format = collector.negotiate_format(request)
-
17
_process_format(format)
-
17
response = collector.response
-
17
response ? response.call : render({})
-
else
-
raise ActionController::UnknownFormat
-
end
-
end
-
-
# A container for responses available from the current controller for
-
# requests for different mime-types sent to a particular action.
-
#
-
# The public controller methods +respond_to+ may be called with a block
-
# that is used to define responses to different mime-types, e.g.
-
# for +respond_to+ :
-
#
-
# respond_to do |format|
-
# format.html
-
# format.xml { render xml: @people }
-
# end
-
#
-
# In this usage, the argument passed to the block (+format+ above) is an
-
# instance of the ActionController::MimeResponds::Collector class. This
-
# object serves as a container in which available responses can be stored by
-
# calling any of the dynamically generated, mime-type-specific methods such
-
# as +html+, +xml+ etc on the Collector. Each response is represented by a
-
# corresponding block if present.
-
#
-
# A subsequent call to #negotiate_format(request) will enable the Collector
-
# to determine which specific mime-type it should respond with for the current
-
# request, with this response then being accessible by calling #response.
-
2
class Collector
-
2
include AbstractController::Collector
-
2
attr_accessor :format
-
-
2
def initialize(mimes, variant = nil)
-
17
@responses = {}
-
17
@variant = variant
-
-
17
mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
-
end
-
-
2
def any(*args, &block)
-
if args.any?
-
args.each { |type| send(type, &block) }
-
else
-
custom(Mime::ALL, &block)
-
end
-
end
-
2
alias :all :any
-
-
2
def custom(mime_type, &block)
-
34
mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
-
34
@responses[mime_type] ||= if block_given?
-
34
block
-
else
-
VariantCollector.new(@variant)
-
end
-
end
-
-
2
def response
-
17
response = @responses.fetch(format, @responses[Mime::ALL])
-
17
if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
-
response.variant
-
17
elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
-
17
response
-
else # `format.html{ |variant| variant.phone }` - variant block syntax
-
variant_collector = VariantCollector.new(@variant)
-
response.call(variant_collector) # call format block with variants collector
-
variant_collector.variant
-
end
-
end
-
-
2
def negotiate_format(request)
-
17
@format = request.negotiate_mime(@responses.keys)
-
end
-
-
2
class VariantCollector #:nodoc:
-
2
def initialize(variant = nil)
-
@variant = variant
-
@variants = {}
-
end
-
-
2
def any(*args, &block)
-
if block_given?
-
if args.any? && args.none?{ |a| a == @variant }
-
args.each{ |v| @variants[v] = block }
-
else
-
@variants[:any] = block
-
end
-
end
-
end
-
2
alias :all :any
-
-
2
def method_missing(name, *args, &block)
-
@variants[name] = block if block_given?
-
end
-
-
2
def variant
-
if @variant.nil?
-
@variants[:none] || @variants[:any]
-
elsif (@variants.keys & @variant).any?
-
@variant.each do |v|
-
return @variants[v] if @variants.key?(v)
-
end
-
else
-
@variants[:any]
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/slice'
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/module/anonymous'
-
2
require 'active_support/core_ext/struct'
-
2
require 'action_dispatch/http/mime_type'
-
-
2
module ActionController
-
# Wraps the parameters hash into a nested hash. This will allow clients to
-
# submit requests without having to specify any root elements.
-
#
-
# This functionality is enabled in +config/initializers/wrap_parameters.rb+
-
# and can be customized. If you are upgrading to \Rails 3.1, this file will
-
# need to be created for the functionality to be enabled.
-
#
-
# You could also turn it on per controller by setting the format array to
-
# a non-empty array:
-
#
-
# class UsersController < ApplicationController
-
# wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
-
# end
-
#
-
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to
-
# send JSON parameters like this:
-
#
-
# {"user": {"name": "Konata"}}
-
#
-
# You can send parameters like this:
-
#
-
# {"name": "Konata"}
-
#
-
# And it will be wrapped into a nested hash with the key name matching the
-
# controller's name. For example, if you're posting to +UsersController+,
-
# your new +params+ hash will look like this:
-
#
-
# {"name" => "Konata", "user" => {"name" => "Konata"}}
-
#
-
# You can also specify the key in which the parameters should be wrapped to,
-
# and also the list of attributes it should wrap by using either +:include+ or
-
# +:exclude+ options like this:
-
#
-
# class UsersController < ApplicationController
-
# wrap_parameters :person, include: [:username, :password]
-
# end
-
#
-
# On ActiveRecord models with no +:include+ or +:exclude+ option set,
-
# it will only wrap the parameters returned by the class method
-
# <tt>attribute_names</tt>.
-
#
-
# If you're going to pass the parameters to an +ActiveModel+ object (such as
-
# <tt>User.new(params[:user])</tt>), you might consider passing the model class to
-
# the method instead. The +ParamsWrapper+ will actually try to determine the
-
# list of attribute names from the model and only wrap those attributes:
-
#
-
# class UsersController < ApplicationController
-
# wrap_parameters Person
-
# end
-
#
-
# You still could pass +:include+ and +:exclude+ to set the list of attributes
-
# you want to wrap.
-
#
-
# By default, if you don't specify the key in which the parameters would be
-
# wrapped to, +ParamsWrapper+ will actually try to determine if there's
-
# a model related to it or not. This controller, for example:
-
#
-
# class Admin::UsersController < ApplicationController
-
# end
-
#
-
# will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
-
# determine the wrapper key respectively. If both models don't exist,
-
# it will then fallback to use +user+ as the key.
-
2
module ParamsWrapper
-
2
extend ActiveSupport::Concern
-
-
2
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
-
-
2
require 'mutex_m'
-
-
2
class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
-
2
include Mutex_m
-
-
2
def self.from_hash(hash)
-
4
name = hash[:name]
-
4
format = Array(hash[:format])
-
4
include = hash[:include] && Array(hash[:include]).collect(&:to_s)
-
4
exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
-
4
new name, format, include, exclude, nil, nil
-
end
-
-
2
def initialize(name, format, include, exclude, klass, model) # nodoc
-
4
super
-
4
@include_set = include
-
4
@name_set = name
-
end
-
-
2
def model
-
super || synchronize { super || self.model = _default_wrap_model }
-
end
-
-
2
def include
-
return super if @include_set
-
-
m = model
-
synchronize do
-
return super if @include_set
-
-
@include_set = true
-
-
unless super || exclude
-
if m.respond_to?(:attribute_names) && m.attribute_names.any?
-
self.include = m.attribute_names
-
end
-
end
-
end
-
end
-
-
2
def name
-
return super if @name_set
-
-
m = model
-
synchronize do
-
return super if @name_set
-
-
@name_set = true
-
-
unless super || klass.anonymous?
-
self.name = m ? m.to_s.demodulize.underscore :
-
klass.controller_name.singularize
-
end
-
end
-
end
-
-
2
private
-
# Determine the wrapper model from the controller's name. By convention,
-
# this could be done by trying to find the defined model that has the
-
# same singularize name as the controller. For example, +UsersController+
-
# will try to find if the +User+ model exists.
-
#
-
# This method also does namespace lookup. Foo::Bar::UsersController will
-
# try to find Foo::Bar::User, Foo::User and finally User.
-
2
def _default_wrap_model #:nodoc:
-
return nil if klass.anonymous?
-
model_name = klass.name.sub(/Controller$/, '').classify
-
-
begin
-
if model_klass = model_name.safe_constantize
-
model_klass
-
else
-
namespaces = model_name.split("::")
-
namespaces.delete_at(-2)
-
break if namespaces.last == model_name
-
model_name = namespaces.join("::")
-
end
-
end until model_klass
-
-
model_klass
-
end
-
end
-
-
2
included do
-
2
class_attribute :_wrapper_options
-
2
self._wrapper_options = Options.from_hash(format: [])
-
end
-
-
2
module ClassMethods
-
2
def _set_wrapper_options(options)
-
self._wrapper_options = Options.from_hash(options)
-
end
-
-
# Sets the name of the wrapper key, or the model which +ParamsWrapper+
-
# would use to determine the attribute names from.
-
#
-
# ==== Examples
-
# wrap_parameters format: :xml
-
# # enables the parameter wrapper for XML format
-
#
-
# wrap_parameters :person
-
# # wraps parameters into +params[:person]+ hash
-
#
-
# wrap_parameters Person
-
# # wraps parameters by determining the wrapper key from Person class
-
# (+person+, in this case) and the list of attribute names
-
#
-
# wrap_parameters include: [:username, :title]
-
# # wraps only +:username+ and +:title+ attributes from parameters.
-
#
-
# wrap_parameters false
-
# # disables parameters wrapping for this controller altogether.
-
#
-
# ==== Options
-
# * <tt>:format</tt> - The list of formats in which the parameters wrapper
-
# will be enabled.
-
# * <tt>:include</tt> - The list of attribute names which parameters wrapper
-
# will wrap into a nested hash.
-
# * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
-
# will exclude from a nested hash.
-
2
def wrap_parameters(name_or_model_or_options, options = {})
-
2
model = nil
-
-
2
case name_or_model_or_options
-
when Hash
-
2
options = name_or_model_or_options
-
when false
-
options = options.merge(:format => [])
-
when Symbol, String
-
options = options.merge(:name => name_or_model_or_options)
-
else
-
model = name_or_model_or_options
-
end
-
-
2
opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
-
2
opts.model = model
-
2
opts.klass = self
-
-
2
self._wrapper_options = opts
-
end
-
-
# Sets the default wrapper key or model which will be used to determine
-
# wrapper key and attribute names. Will be called automatically when the
-
# module is inherited.
-
2
def inherited(klass)
-
8
if klass._wrapper_options.format.any?
-
8
params = klass._wrapper_options.dup
-
8
params.klass = klass
-
8
klass._wrapper_options = params
-
end
-
8
super
-
end
-
end
-
-
# Performs parameters wrapping upon the request. Will be called automatically
-
# by the metal call stack.
-
2
def process_action(*args)
-
29
if _wrapper_enabled?
-
if request.parameters[_wrapper_key].present?
-
wrapped_hash = _extract_parameters(request.parameters)
-
else
-
wrapped_hash = _wrap_parameters request.request_parameters
-
end
-
-
wrapped_keys = request.request_parameters.keys
-
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
-
-
# This will make the wrapped hash accessible from controller and view
-
request.parameters.merge! wrapped_hash
-
request.request_parameters.merge! wrapped_hash
-
-
# This will display the wrapped hash in the log file
-
request.filtered_parameters.merge! wrapped_filtered_hash
-
end
-
29
super
-
end
-
-
2
private
-
-
# Returns the wrapper key which will be used to stored wrapped parameters.
-
2
def _wrapper_key
-
_wrapper_options.name
-
end
-
-
# Returns the list of enabled formats.
-
2
def _wrapper_formats
-
29
_wrapper_options.format
-
end
-
-
# Returns the list of parameters which will be selected for wrapped.
-
2
def _wrap_parameters(parameters)
-
{ _wrapper_key => _extract_parameters(parameters) }
-
end
-
-
2
def _extract_parameters(parameters)
-
if include_only = _wrapper_options.include
-
parameters.slice(*include_only)
-
else
-
exclude = _wrapper_options.exclude || []
-
parameters.except(*(exclude + EXCLUDE_PARAMETERS))
-
end
-
end
-
-
# Checks if we should perform parameters wrapping.
-
2
def _wrapper_enabled?
-
29
ref = request.content_mime_type.try(:ref)
-
29
_wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]
-
end
-
end
-
end
-
2
require 'action_dispatch/http/request'
-
2
require 'action_dispatch/http/response'
-
-
2
module ActionController
-
2
module RackDelegation
-
2
extend ActiveSupport::Concern
-
-
2
delegate :headers, :status=, :location=, :content_type=,
-
:status, :location, :content_type, :response_code, :to => "@_response"
-
-
2
def dispatch(action, request)
-
set_response!(request)
-
super(action, request)
-
end
-
-
2
def response_body=(body)
-
29
response.body = body if response
-
29
super
-
end
-
-
2
def reset_session
-
@_request.reset_session
-
end
-
-
2
private
-
-
2
def set_response!(request)
-
@_response = ActionDispatch::Response.new
-
@_response.request = request
-
end
-
end
-
end
-
2
module ActionController
-
2
class RedirectBackError < AbstractController::Error #:nodoc:
-
2
DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].'
-
-
2
def initialize(message = nil)
-
super(message || DEFAULT_MESSAGE)
-
end
-
end
-
-
2
module Redirecting
-
2
extend ActiveSupport::Concern
-
-
2
include AbstractController::Logger
-
2
include ActionController::RackDelegation
-
2
include ActionController::UrlFor
-
-
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
-
#
-
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
-
# * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
-
# * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
-
# * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
-
# * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
-
# * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places.
-
# Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt>
-
#
-
# === Examples:
-
#
-
# redirect_to action: "show", id: 5
-
# redirect_to post
-
# redirect_to "http://www.rubyonrails.org"
-
# redirect_to "/images/screenshot.jpg"
-
# redirect_to articles_url
-
# redirect_to :back
-
# redirect_to proc { edit_post_url(@post) }
-
#
-
# The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option:
-
#
-
# redirect_to post_url(@post), status: :found
-
# redirect_to action: 'atom', status: :moved_permanently
-
# redirect_to post_url(@post), status: 301
-
# redirect_to action: 'atom', status: 302
-
#
-
# The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
-
# integer, or a symbol representing the downcased, underscored and symbolized description.
-
# Note that the status code must be a 3xx HTTP code, or redirection will not occur.
-
#
-
# If you are using XHR requests other than GET or POST and redirecting after the
-
# request then some browsers will follow the redirect using the original request
-
# method. This may lead to undesirable behavior such as a double DELETE. To work
-
# around this you can return a <tt>303 See Other</tt> status code which will be
-
# followed using a GET request.
-
#
-
# redirect_to posts_url, status: :see_other
-
# redirect_to action: 'index', status: 303
-
#
-
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
-
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
-
#
-
# redirect_to post_url(@post), alert: "Watch it, mister!"
-
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
-
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
-
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
-
#
-
# When using <tt>redirect_to :back</tt>, if there is no referrer,
-
# <tt>ActionController::RedirectBackError</tt> will be raised. You
-
# may specify some fallback behavior for this case by rescuing
-
# <tt>ActionController::RedirectBackError</tt>.
-
2
def redirect_to(options = {}, response_status = {}) #:doc:
-
13
raise ActionControllerError.new("Cannot redirect to nil!") unless options
-
13
raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters)
-
13
raise AbstractController::DoubleRenderError if response_body
-
-
13
self.status = _extract_redirect_to_status(options, response_status)
-
13
self.location = _compute_redirect_to_location(request, options)
-
13
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
-
end
-
-
2
def _compute_redirect_to_location(request, options) #:nodoc:
-
case options
-
# The scheme name consist of a letter followed by any combination of
-
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
-
# characters; and is terminated by a colon (":").
-
# See http://tools.ietf.org/html/rfc3986#section-3.1
-
# The protocol relative scheme starts with a double slash "//".
-
when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
-
12
options
-
when String
-
13
request.protocol + request.host_with_port + options
-
when :back
-
request.headers["Referer"] or raise RedirectBackError
-
when Proc
-
_compute_redirect_to_location request, options.call
-
else
-
6
url_for(options)
-
31
end.delete("\0\r\n")
-
end
-
2
module_function :_compute_redirect_to_location
-
2
public :_compute_redirect_to_location
-
-
2
private
-
2
def _extract_redirect_to_status(options, response_status)
-
13
if options.is_a?(Hash) && options.key?(:status)
-
Rack::Utils.status_code(options.delete(:status))
-
13
elsif response_status.key?(:status)
-
Rack::Utils.status_code(response_status[:status])
-
else
-
13
302
-
end
-
end
-
end
-
end
-
2
require 'set'
-
-
2
module ActionController
-
# See <tt>Renderers.add</tt>
-
2
def self.add_renderer(key, &block)
-
Renderers.add(key, &block)
-
end
-
-
# See <tt>Renderers.remove</tt>
-
2
def self.remove_renderer(key)
-
Renderers.remove(key)
-
end
-
-
2
class MissingRenderer < LoadError
-
2
def initialize(format)
-
super "No renderer defined for format: #{format}"
-
end
-
end
-
-
2
module Renderers
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :_renderers
-
2
self._renderers = Set.new.freeze
-
end
-
-
2
module ClassMethods
-
2
def use_renderers(*args)
-
renderers = _renderers + args
-
self._renderers = renderers.freeze
-
end
-
2
alias use_renderer use_renderers
-
end
-
-
2
def render_to_body(options)
-
16
_render_to_body_with_renderer(options) || super
-
end
-
-
2
def _render_to_body_with_renderer(options)
-
16
_renderers.each do |name|
-
48
if options.key?(name)
-
_process_options(options)
-
method_name = Renderers._render_with_renderer_method_name(name)
-
return send(method_name, options.delete(name), options)
-
end
-
end
-
nil
-
end
-
-
# A Set containing renderer names that correspond to available renderer procs.
-
# Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
-
2
RENDERERS = Set.new
-
-
2
def self._render_with_renderer_method_name(key)
-
6
"_render_with_renderer_#{key}"
-
end
-
-
# Adds a new renderer to call within controller actions.
-
# A renderer is invoked by passing its name as an option to
-
# <tt>AbstractController::Rendering#render</tt>. To create a renderer
-
# pass it a name and a block. The block takes two arguments, the first
-
# is the value paired with its key and the second is the remaining
-
# hash of options passed to +render+.
-
#
-
# Create a csv renderer:
-
#
-
# ActionController::Renderers.add :csv do |obj, options|
-
# filename = options[:filename] || 'data'
-
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
-
# send_data str, type: Mime::CSV,
-
# disposition: "attachment; filename=#{filename}.csv"
-
# end
-
#
-
# Note that we used Mime::CSV for the csv mime type as it comes with Rails.
-
# For a custom renderer, you'll need to register a mime type with
-
# <tt>Mime::Type.register</tt>.
-
#
-
# To use the csv renderer in a controller action:
-
#
-
# def show
-
# @csvable = Csvable.find(params[:id])
-
# respond_to do |format|
-
# format.html
-
# format.csv { render csv: @csvable, filename: @csvable.name }
-
# end
-
# end
-
# To use renderers and their mime types in more concise ways, see
-
# <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt>
-
2
def self.add(key, &block)
-
6
define_method(_render_with_renderer_method_name(key), &block)
-
6
RENDERERS << key.to_sym
-
end
-
-
# This method is the opposite of add method.
-
#
-
# Usage:
-
#
-
# ActionController::Renderers.remove(:csv)
-
2
def self.remove(key)
-
RENDERERS.delete(key.to_sym)
-
method_name = _render_with_renderer_method_name(key)
-
remove_method(method_name) if method_defined?(method_name)
-
end
-
-
2
module All
-
2
extend ActiveSupport::Concern
-
2
include Renderers
-
-
2
included do
-
2
self._renderers = RENDERERS
-
end
-
end
-
-
2
add :json do |json, options|
-
json = json.to_json(options) unless json.kind_of?(String)
-
-
if options[:callback].present?
-
if content_type.nil? || content_type == Mime::JSON
-
self.content_type = Mime::JS
-
end
-
-
"/**/#{options[:callback]}(#{json})"
-
else
-
self.content_type ||= Mime::JSON
-
json
-
end
-
end
-
-
2
add :js do |js, options|
-
self.content_type ||= Mime::JS
-
js.respond_to?(:to_js) ? js.to_js(options) : js
-
end
-
-
2
add :xml do |xml, options|
-
self.content_type ||= Mime::XML
-
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
-
end
-
end
-
end
-
2
module ActionController
-
2
module Rendering
-
2
extend ActiveSupport::Concern
-
-
2
RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
-
-
# Before processing, set the request formats in current controller formats.
-
2
def process_action(*) #:nodoc:
-
29
self.formats = request.formats.map(&:ref).compact
-
29
super
-
end
-
-
# Check for double render errors and set the content_type after rendering.
-
2
def render(*args) #:nodoc:
-
16
raise ::AbstractController::DoubleRenderError if self.response_body
-
16
super
-
end
-
-
# Overwrite render_to_string because body can now be set to a rack body.
-
2
def render_to_string(*)
-
result = super
-
if result.respond_to?(:each)
-
string = ""
-
result.each { |r| string << r }
-
string
-
else
-
result
-
end
-
end
-
-
2
def render_to_body(options = {})
-
16
super || _render_in_priorities(options) || ' '
-
end
-
-
2
private
-
-
2
def _render_in_priorities(options)
-
RENDER_FORMATS_IN_PRIORITY.each do |format|
-
return options[format] if options.key?(format)
-
end
-
-
nil
-
end
-
-
2
def _process_format(format, options = {})
-
33
super
-
-
33
if options[:plain]
-
self.content_type = Mime::TEXT
-
else
-
33
self.content_type ||= format.to_s
-
end
-
end
-
-
# Normalize arguments by catching blocks and setting them on :update.
-
2
def _normalize_args(action=nil, options={}, &blk) #:nodoc:
-
16
options = super
-
16
options[:update] = blk if block_given?
-
16
options
-
end
-
-
# Normalize both text and status options.
-
2
def _normalize_options(options) #:nodoc:
-
16
_normalize_text(options)
-
-
16
if options[:html]
-
options[:html] = ERB::Util.html_escape(options[:html])
-
end
-
-
16
if options.delete(:nothing)
-
options[:body] = nil
-
end
-
-
16
if options[:status]
-
options[:status] = Rack::Utils.status_code(options[:status])
-
end
-
-
16
super
-
end
-
-
2
def _normalize_text(options)
-
16
RENDER_FORMATS_IN_PRIORITY.each do |format|
-
64
if options.key?(format) && options[format].respond_to?(:to_text)
-
options[format] = options[format].to_text
-
end
-
end
-
end
-
-
# Process controller specific options, as status, content-type and location.
-
2
def _process_options(options) #:nodoc:
-
16
status, content_type, location = options.values_at(:status, :content_type, :location)
-
-
16
self.status = status if status
-
16
self.content_type = content_type if content_type
-
16
self.headers["Location"] = url_for(location) if location
-
-
16
super
-
end
-
end
-
end
-
2
require 'rack/session/abstract/id'
-
2
require 'action_controller/metal/exceptions'
-
2
require 'active_support/security_utils'
-
-
2
module ActionController #:nodoc:
-
2
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
-
end
-
-
2
class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
-
end
-
-
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
-
# by including a token in the rendered HTML for your application. This token is
-
# stored as a random string in the session, to which an attacker does not have
-
# access. When a request reaches your application, \Rails verifies the received
-
# token with the token in the session. Only HTML and JavaScript requests are checked,
-
# so this will not protect your XML API (presumably you'll have a different
-
# authentication scheme there anyway).
-
#
-
# GET requests are not protected since they don't have side effects like writing
-
# to the database and don't leak sensitive information. JavaScript requests are
-
# an exception: a third-party site can use a <script> tag to reference a JavaScript
-
# URL on your site. When your JavaScript response loads on their site, it executes.
-
# With carefully crafted JavaScript on their end, sensitive data in your JavaScript
-
# response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
-
# Ajax) requests are allowed to make GET requests for JavaScript responses.
-
#
-
# It's important to remember that XML or JSON requests are also affected and if
-
# you're building an API you'll need something like:
-
#
-
# class ApplicationController < ActionController::Base
-
# protect_from_forgery
-
# skip_before_action :verify_authenticity_token, if: :json_request?
-
#
-
# protected
-
#
-
# def json_request?
-
# request.format.json?
-
# end
-
# end
-
#
-
# CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
-
# which checks the token and resets the session if it doesn't match what was expected.
-
# A call to this method is generated for new \Rails applications by default.
-
#
-
# The token parameter is named <tt>authenticity_token</tt> by default. The name and
-
# value of this token must be added to every layout that renders forms by including
-
# <tt>csrf_meta_tags</tt> in the HTML +head+.
-
#
-
# Learn more about CSRF attacks and securing your application in the
-
# {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
-
2
module RequestForgeryProtection
-
2
extend ActiveSupport::Concern
-
-
2
include AbstractController::Helpers
-
2
include AbstractController::Callbacks
-
-
2
included do
-
# Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
-
# sets it to <tt>:authenticity_token</tt> by default.
-
2
config_accessor :request_forgery_protection_token
-
2
self.request_forgery_protection_token ||= :authenticity_token
-
-
# Holds the class which implements the request forgery protection.
-
2
config_accessor :forgery_protection_strategy
-
2
self.forgery_protection_strategy = nil
-
-
# Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
-
2
config_accessor :allow_forgery_protection
-
2
self.allow_forgery_protection = true if allow_forgery_protection.nil?
-
-
# Controls whether a CSRF failure logs a warning. On by default.
-
2
config_accessor :log_warning_on_csrf_failure
-
2
self.log_warning_on_csrf_failure = true
-
-
2
helper_method :form_authenticity_token
-
2
helper_method :protect_against_forgery?
-
end
-
-
2
module ClassMethods
-
# Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
-
#
-
# class ApplicationController < ActionController::Base
-
# protect_from_forgery
-
# end
-
#
-
# class FooController < ApplicationController
-
# protect_from_forgery except: :index
-
#
-
# You can disable CSRF protection on controller by skipping the verification before_action:
-
# skip_before_action :verify_authenticity_token
-
#
-
# Valid Options:
-
#
-
# * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
-
# * <tt>:with</tt> - Set the method to handle unverified request.
-
#
-
# Valid unverified request handling methods are:
-
# * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
-
# * <tt>:reset_session</tt> - Resets the session.
-
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
-
2
def protect_from_forgery(options = {})
-
2
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
-
2
self.request_forgery_protection_token ||= :authenticity_token
-
2
prepend_before_action :verify_authenticity_token, options
-
2
append_after_action :verify_same_origin_request
-
end
-
-
2
private
-
-
2
def protection_method_class(name)
-
2
ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
-
rescue NameError
-
raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session'
-
end
-
end
-
-
2
module ProtectionMethods
-
2
class NullSession
-
2
def initialize(controller)
-
@controller = controller
-
end
-
-
# This is the method that defines the application behavior when a request is found to be unverified.
-
2
def handle_unverified_request
-
request = @controller.request
-
request.session = NullSessionHash.new(request.env)
-
request.env['action_dispatch.request.flash_hash'] = nil
-
request.env['rack.session.options'] = { skip: true }
-
request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
-
end
-
-
2
protected
-
-
2
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
-
2
def initialize(env)
-
super(nil, env)
-
@data = {}
-
@loaded = true
-
end
-
-
# no-op
-
2
def destroy; end
-
-
2
def exists?
-
true
-
end
-
end
-
-
2
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
-
2
def self.build(request)
-
key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
-
host = request.host
-
secure = request.ssl?
-
-
new(key_generator, host, secure, options_for_env({}))
-
end
-
-
2
def write(*)
-
# nothing
-
end
-
end
-
end
-
-
2
class ResetSession
-
2
def initialize(controller)
-
@controller = controller
-
end
-
-
2
def handle_unverified_request
-
@controller.reset_session
-
end
-
end
-
-
2
class Exception
-
2
def initialize(controller)
-
@controller = controller
-
end
-
-
2
def handle_unverified_request
-
raise ActionController::InvalidAuthenticityToken
-
end
-
end
-
end
-
-
2
protected
-
# The actual before_action that is used to verify the CSRF token.
-
# Don't override this directly. Provide your own forgery protection
-
# strategy instead. If you override, you'll disable same-origin
-
# `<script>` verification.
-
#
-
# Lean on the protect_from_forgery declaration to mark which actions are
-
# due for same-origin request verification. If protect_from_forgery is
-
# enabled on an action, this before_action flags its after_action to
-
# verify that JavaScript responses are for XHR requests, ensuring they
-
# follow the browser's same-origin policy.
-
2
def verify_authenticity_token
-
29
mark_for_same_origin_verification!
-
-
29
if !verified_request?
-
if logger && log_warning_on_csrf_failure
-
logger.warn "Can't verify CSRF token authenticity"
-
end
-
handle_unverified_request
-
end
-
end
-
-
2
def handle_unverified_request
-
forgery_protection_strategy.new(self).handle_unverified_request
-
end
-
-
#:nodoc:
-
2
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
-
"<script> tag on another site requested protected JavaScript. " \
-
"If you know what you're doing, go ahead and disable forgery " \
-
"protection on this action to permit cross-origin JavaScript embedding."
-
2
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
-
-
# If `verify_authenticity_token` was run (indicating that we have
-
# forgery protection enabled for this request) then also verify that
-
# we aren't serving an unauthorized cross-origin response.
-
2
def verify_same_origin_request
-
29
if marked_for_same_origin_verification? && non_xhr_javascript_response?
-
logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
-
raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
-
end
-
end
-
-
# GET requests are checked for cross-origin JavaScript after rendering.
-
2
def mark_for_same_origin_verification!
-
29
@marked_for_same_origin_verification = request.get?
-
end
-
-
# If the `verify_authenticity_token` before_action ran, verify that
-
# JavaScript responses are only served to same-origin GET requests.
-
2
def marked_for_same_origin_verification?
-
29
@marked_for_same_origin_verification ||= false
-
end
-
-
# Check for cross-origin JavaScript responses.
-
2
def non_xhr_javascript_response?
-
12
content_type =~ %r(\Atext/javascript) && !request.xhr?
-
end
-
-
2
AUTHENTICITY_TOKEN_LENGTH = 32
-
-
# Returns true or false if a request is verified. Checks:
-
#
-
# * is it a GET or HEAD request? Gets should be safe and idempotent
-
# * Does the form_authenticity_token match the given token value from the params?
-
# * Does the X-CSRF-Token header match the form_authenticity_token
-
2
def verified_request?
-
29
!protect_against_forgery? || request.get? || request.head? ||
-
valid_authenticity_token?(session, form_authenticity_param) ||
-
valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
-
end
-
-
# Sets the token value for the current session.
-
2
def form_authenticity_token
-
masked_authenticity_token(session)
-
end
-
-
# Creates a masked version of the authenticity token that varies
-
# on each request. The masking is used to mitigate SSL attacks
-
# like BREACH.
-
2
def masked_authenticity_token(session)
-
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
-
encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
-
masked_token = one_time_pad + encrypted_csrf_token
-
Base64.strict_encode64(masked_token)
-
end
-
-
# Checks the client's masked token to see if it matches the
-
# session token. Essentially the inverse of
-
# +masked_authenticity_token+.
-
2
def valid_authenticity_token?(session, encoded_masked_token)
-
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
-
return false
-
end
-
-
begin
-
masked_token = Base64.strict_decode64(encoded_masked_token)
-
rescue ArgumentError # encoded_masked_token is invalid Base64
-
return false
-
end
-
-
# See if it's actually a masked token or not. In order to
-
# deploy this code, we should be able to handle any unmasked
-
# tokens that we've issued without error.
-
-
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
-
# This is actually an unmasked token. This is expected if
-
# you have just upgraded to masked tokens, but should stop
-
# happening shortly after installing this gem
-
compare_with_real_token masked_token, session
-
-
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
-
# Split the token into the one-time pad and the encrypted
-
# value and decrypt it
-
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
-
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
-
csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
-
-
compare_with_real_token csrf_token, session
-
-
else
-
false # Token is malformed
-
end
-
end
-
-
2
def compare_with_real_token(token, session)
-
ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
-
end
-
-
2
def real_csrf_token(session)
-
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
-
Base64.strict_decode64(session[:_csrf_token])
-
end
-
-
2
def xor_byte_strings(s1, s2)
-
s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
-
end
-
-
# The form's authenticity parameter. Override to provide your own.
-
2
def form_authenticity_param
-
params[request_forgery_protection_token]
-
end
-
-
# Checks if the controller allows forgery protection.
-
2
def protect_against_forgery?
-
55
allow_forgery_protection
-
end
-
end
-
end
-
2
module ActionController #:nodoc:
-
# This module is responsible to provide `rescue_from` helpers
-
# to controllers and configure when detailed exceptions must be
-
# shown.
-
2
module Rescue
-
2
extend ActiveSupport::Concern
-
2
include ActiveSupport::Rescuable
-
-
2
def rescue_with_handler(exception)
-
if (exception.respond_to?(:original_exception) &&
-
(orig_exception = exception.original_exception) &&
-
handler_for_rescue(orig_exception))
-
exception = orig_exception
-
end
-
super(exception)
-
end
-
-
# Override this method if you want to customize when detailed
-
# exceptions must be shown. This method is only called when
-
# consider_all_requests_local is false. By default, it returns
-
# false, but someone may set it to `request.local?` so local
-
# requests in production still shows the detailed exception pages.
-
2
def show_detailed_exceptions?
-
false
-
end
-
-
2
private
-
2
def process_action(*args)
-
29
super
-
rescue Exception => exception
-
request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions?
-
rescue_with_handler(exception) || raise(exception)
-
end
-
end
-
end
-
2
require 'rack/chunked'
-
-
2
module ActionController #:nodoc:
-
# Allows views to be streamed back to the client as they are rendered.
-
#
-
# The default way Rails renders views is by first rendering the template
-
# and then the layout. The response is sent to the client after the whole
-
# template is rendered, all queries are made, and the layout is processed.
-
#
-
# Streaming inverts the rendering flow by rendering the layout first and
-
# streaming each part of the layout as they are processed. This allows the
-
# header of the HTML (which is usually in the layout) to be streamed back
-
# to client very quickly, allowing JavaScripts and stylesheets to be loaded
-
# earlier than usual.
-
#
-
# This approach was introduced in Rails 3.1 and is still improving. Several
-
# Rack middlewares may not work and you need to be careful when streaming.
-
# Those points are going to be addressed soon.
-
#
-
# In order to use streaming, you will need to use a Ruby version that
-
# supports fibers (fibers are supported since version 1.9.2 of the main
-
# Ruby implementation).
-
#
-
# Streaming can be added to a given template easily, all you need to do is
-
# to pass the :stream option.
-
#
-
# class PostsController
-
# def index
-
# @posts = Post.all
-
# render stream: true
-
# end
-
# end
-
#
-
# == When to use streaming
-
#
-
# Streaming may be considered to be overkill for lightweight actions like
-
# +new+ or +edit+. The real benefit of streaming is on expensive actions
-
# that, for example, do a lot of queries on the database.
-
#
-
# In such actions, you want to delay queries execution as much as you can.
-
# For example, imagine the following +dashboard+ action:
-
#
-
# def dashboard
-
# @posts = Post.all
-
# @pages = Page.all
-
# @articles = Article.all
-
# end
-
#
-
# Most of the queries here are happening in the controller. In order to benefit
-
# from streaming you would want to rewrite it as:
-
#
-
# def dashboard
-
# # Allow lazy execution of the queries
-
# @posts = Post.all
-
# @pages = Page.all
-
# @articles = Article.all
-
# render stream: true
-
# end
-
#
-
# Notice that :stream only works with templates. Rendering :json
-
# or :xml with :stream won't work.
-
#
-
# == Communication between layout and template
-
#
-
# When streaming, rendering happens top-down instead of inside-out.
-
# Rails starts with the layout, and the template is rendered later,
-
# when its +yield+ is reached.
-
#
-
# This means that, if your application currently relies on instance
-
# variables set in the template to be used in the layout, they won't
-
# work once you move to streaming. The proper way to communicate
-
# between layout and template, regardless of whether you use streaming
-
# or not, is by using +content_for+, +provide+ and +yield+.
-
#
-
# Take a simple example where the layout expects the template to tell
-
# which title to use:
-
#
-
# <html>
-
# <head><title><%= yield :title %></title></head>
-
# <body><%= yield %></body>
-
# </html>
-
#
-
# You would use +content_for+ in your template to specify the title:
-
#
-
# <%= content_for :title, "Main" %>
-
# Hello
-
#
-
# And the final result would be:
-
#
-
# <html>
-
# <head><title>Main</title></head>
-
# <body>Hello</body>
-
# </html>
-
#
-
# However, if +content_for+ is called several times, the final result
-
# would have all calls concatenated. For instance, if we have the following
-
# template:
-
#
-
# <%= content_for :title, "Main" %>
-
# Hello
-
# <%= content_for :title, " page" %>
-
#
-
# The final result would be:
-
#
-
# <html>
-
# <head><title>Main page</title></head>
-
# <body>Hello</body>
-
# </html>
-
#
-
# This means that, if you have <code>yield :title</code> in your layout
-
# and you want to use streaming, you would have to render the whole template
-
# (and eventually trigger all queries) before streaming the title and all
-
# assets, which kills the purpose of streaming. For this reason Rails 3.1
-
# introduces a new helper called +provide+ that does the same as +content_for+
-
# but tells the layout to stop searching for other entries and continue rendering.
-
#
-
# For instance, the template above using +provide+ would be:
-
#
-
# <%= provide :title, "Main" %>
-
# Hello
-
# <%= content_for :title, " page" %>
-
#
-
# Giving:
-
#
-
# <html>
-
# <head><title>Main</title></head>
-
# <body>Hello</body>
-
# </html>
-
#
-
# That said, when streaming, you need to properly check your templates
-
# and choose when to use +provide+ and +content_for+.
-
#
-
# == Headers, cookies, session and flash
-
#
-
# When streaming, the HTTP headers are sent to the client right before
-
# it renders the first line. This means that, modifying headers, cookies,
-
# session or flash after the template starts rendering will not propagate
-
# to the client.
-
#
-
# == Middlewares
-
#
-
# Middlewares that need to manipulate the body won't work with streaming.
-
# You should disable those middlewares whenever streaming in development
-
# or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it
-
# needs to inject contents in the HTML body.
-
#
-
# Also <tt>Rack::Cache</tt> won't work with streaming as it does not support
-
# streaming bodies yet. Whenever streaming Cache-Control is automatically
-
# set to "no-cache".
-
#
-
# == Errors
-
#
-
# When it comes to streaming, exceptions get a bit more complicated. This
-
# happens because part of the template was already rendered and streamed to
-
# the client, making it impossible to render a whole exception page.
-
#
-
# Currently, when an exception happens in development or production, Rails
-
# will automatically stream to the client:
-
#
-
# "><script>window.location = "/500.html"</script></html>
-
#
-
# The first two characters (">) are required in case the exception happens
-
# while rendering attributes for a given tag. You can check the real cause
-
# for the exception in your logger.
-
#
-
# == Web server support
-
#
-
# Not all web servers support streaming out-of-the-box. You need to check
-
# the instructions for each of them.
-
#
-
# ==== Unicorn
-
#
-
# Unicorn supports streaming but it needs to be configured. For this, you
-
# need to create a config file as follow:
-
#
-
# # unicorn.config.rb
-
# listen 3000, tcp_nopush: false
-
#
-
# And use it on initialization:
-
#
-
# unicorn_rails --config-file unicorn.config.rb
-
#
-
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
-
# Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
-
#
-
# If you are using Unicorn with NGINX, you may need to tweak NGINX.
-
# Streaming should work out of the box on Rainbows.
-
#
-
# ==== Passenger
-
#
-
# To be described.
-
#
-
2
module Streaming
-
2
extend ActiveSupport::Concern
-
-
2
protected
-
-
# Set proper cache control and transfer encoding when streaming
-
2
def _process_options(options) #:nodoc:
-
16
super
-
16
if options[:stream]
-
if env["HTTP_VERSION"] == "HTTP/1.0"
-
options.delete(:stream)
-
else
-
headers["Cache-Control"] ||= "no-cache"
-
headers["Transfer-Encoding"] = "chunked"
-
headers.delete("Content-Length")
-
end
-
end
-
end
-
-
# Call render_body if we are streaming instead of usual +render+.
-
2
def _render_template(options) #:nodoc:
-
16
if options.delete(:stream)
-
Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
-
else
-
16
super
-
end
-
end
-
end
-
end
-
1
module ActionController
-
1
module Testing
-
1
extend ActiveSupport::Concern
-
-
1
include RackDelegation
-
-
# TODO : Rewrite tests using controller.headers= to use Rack env
-
1
def headers=(new_headers)
-
@_response ||= ActionDispatch::Response.new
-
@_response.headers.replace(new_headers)
-
end
-
-
# Behavior specific to functional tests
-
1
module Functional # :nodoc:
-
1
def set_response!(request)
-
end
-
-
1
def recycle!
-
58
@_url_options = nil
-
58
self.formats = nil
-
58
self.params = nil
-
end
-
end
-
-
1
module ClassMethods
-
1
def before_filters
-
_process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name}
-
end
-
end
-
end
-
end
-
2
module ActionController
-
# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
-
# the <tt>_routes</tt> method. Otherwise, an exception will be raised.
-
#
-
# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
-
# url options like the +host+. In order to do so, this module requires the host class
-
# to implement +env+ and +request+, which need to be a Rack-compatible.
-
#
-
# class RootUrl
-
# include ActionController::UrlFor
-
# include Rails.application.routes.url_helpers
-
#
-
# delegate :env, :request, to: :controller
-
#
-
# def initialize(controller)
-
# @controller = controller
-
# @url = root_path # named route from the application.
-
# end
-
# end
-
2
module UrlFor
-
2
extend ActiveSupport::Concern
-
-
2
include AbstractController::UrlFor
-
-
2
def url_options
-
@_url_options ||= {
-
:host => request.host,
-
:port => request.optional_port,
-
:protocol => request.protocol,
-
:_recall => request.path_parameters
-
208
}.merge!(super).freeze
-
-
208
if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) ||
-
416
(script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) ||
-
208
(original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze])
-
-
options = @_url_options.dup
-
if original_script_name
-
options[:original_script_name] = original_script_name
-
else
-
options[:script_name] = same_origin ? request.script_name.dup : script_name
-
end
-
options.freeze
-
else
-
208
@_url_options
-
end
-
end
-
end
-
end
-
2
require 'rack/session/abstract/id'
-
2
require 'active_support/core_ext/object/to_query'
-
2
require 'active_support/core_ext/module/anonymous'
-
2
require 'active_support/core_ext/hash/keys'
-
2
require 'active_support/deprecation'
-
-
2
require 'rails-dom-testing'
-
-
2
module ActionController
-
2
module TemplateAssertions
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
4
setup :setup_subscriptions
-
4
teardown :teardown_subscriptions
-
end
-
-
2
RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze
-
-
2
def setup_subscriptions
-
29
RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
-
116
instance_variable_set("@_#{instance_variable}", Hash.new(0))
-
end
-
-
29
@_subscribers = []
-
-
@_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload|
-
16
path = payload[:layout]
-
16
if path
-
16
@_layouts[path] += 1
-
16
if path =~ /^layouts\/(.*)/
-
16
@_layouts[$1] += 1
-
end
-
end
-
29
end
-
-
@_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
-
42
if virtual_path = payload[:virtual_path]
-
42
partial = virtual_path =~ /^.*\/_[^\/]*$/
-
-
42
if partial
-
10
@_partials[virtual_path] += 1
-
10
@_partials[virtual_path.split("/").last] += 1
-
end
-
-
42
@_templates[virtual_path] += 1
-
else
-
path = payload[:identifier]
-
if path
-
@_files[path] += 1
-
@_files[path.split("/").last] += 1
-
end
-
end
-
29
end
-
end
-
-
2
def teardown_subscriptions
-
29
return unless defined?(@_subscribers)
-
-
29
@_subscribers.each do |subscriber|
-
58
ActiveSupport::Notifications.unsubscribe(subscriber)
-
end
-
end
-
-
2
def process(*args)
-
29
reset_template_assertion
-
29
super
-
end
-
-
2
def reset_template_assertion
-
29
RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
-
116
ivar_name = "@_#{instance_variable}"
-
116
if instance_variable_defined?(ivar_name)
-
116
instance_variable_get(ivar_name).clear
-
end
-
end
-
end
-
-
# Asserts that the request was rendered with the appropriate template file or partials.
-
#
-
# # assert that the "new" view template was rendered
-
# assert_template "new"
-
#
-
# # assert that the exact template "admin/posts/new" was rendered
-
# assert_template %r{\Aadmin/posts/new\Z}
-
#
-
# # assert that the layout 'admin' was rendered
-
# assert_template layout: 'admin'
-
# assert_template layout: 'layouts/admin'
-
# assert_template layout: :admin
-
#
-
# # assert that no layout was rendered
-
# assert_template layout: nil
-
# assert_template layout: false
-
#
-
# # assert that the "_customer" partial was rendered twice
-
# assert_template partial: '_customer', count: 2
-
#
-
# # assert that no partials were rendered
-
# assert_template partial: false
-
#
-
# # assert that a file was rendered
-
# assert_template file: "README.rdoc"
-
#
-
# # assert that no file was rendered
-
# assert_template file: nil
-
# assert_template file: false
-
#
-
# In a view test case, you can also assert that specific locals are passed
-
# to partials:
-
#
-
# # assert that the "_customer" partial was rendered with a specific object
-
# assert_template partial: '_customer', locals: { customer: @customer }
-
2
def assert_template(options = {}, message = nil)
-
# Force body to be read in case the template is being streamed.
-
5
response.body
-
-
5
case options
-
when NilClass, Regexp, String, Symbol
-
5
options = options.to_s if Symbol === options
-
5
rendered = @_templates
-
5
msg = message || sprintf("expecting <%s> but rendering with <%s>",
-
options.inspect, rendered.keys)
-
5
matches_template =
-
case options
-
when String
-
!options.empty? && rendered.any? do |t, num|
-
6
options_splited = options.split(File::SEPARATOR)
-
6
t_splited = t.split(File::SEPARATOR)
-
6
t_splited.last(options_splited.size) == options_splited
-
5
end
-
when Regexp
-
rendered.any? { |t,num| t.match(options) }
-
when NilClass
-
rendered.blank?
-
end
-
5
assert matches_template, msg
-
when Hash
-
options.assert_valid_keys(:layout, :partial, :locals, :count, :file)
-
-
if options.key?(:layout)
-
expected_layout = options[:layout]
-
msg = message || sprintf("expecting layout <%s> but action rendered <%s>",
-
expected_layout, @_layouts.keys)
-
-
case expected_layout
-
when String, Symbol
-
assert_includes @_layouts.keys, expected_layout.to_s, msg
-
when Regexp
-
assert(@_layouts.keys.any? {|l| l =~ expected_layout }, msg)
-
when nil, false
-
assert(@_layouts.empty?, msg)
-
end
-
end
-
-
if options[:file]
-
assert_includes @_files.keys, options[:file]
-
elsif options.key?(:file)
-
assert @_files.blank?, "expected no files but #{@_files.keys} was rendered"
-
end
-
-
if expected_partial = options[:partial]
-
if expected_locals = options[:locals]
-
if defined?(@_rendered_views)
-
view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/')
-
-
partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view
-
assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg
-
-
msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial,
-
expected_locals,
-
@_rendered_views.locals_for(view)]
-
assert(@_rendered_views.view_rendered?(view, options[:locals]), msg)
-
else
-
warn "the :locals option to #assert_template is only supported in a ActionView::TestCase"
-
end
-
elsif expected_count = options[:count]
-
actual_count = @_partials[expected_partial]
-
msg = message || sprintf("expecting %s to be rendered %s time(s) but rendered %s time(s)",
-
expected_partial, expected_count, actual_count)
-
assert(actual_count == expected_count.to_i, msg)
-
else
-
msg = message || sprintf("expecting partial <%s> but action rendered <%s>",
-
options[:partial], @_partials.keys)
-
assert_includes @_partials, expected_partial, msg
-
end
-
elsif options.key?(:partial)
-
assert @_partials.empty?,
-
"Expected no partials to be rendered"
-
end
-
else
-
raise ArgumentError, "assert_template only accepts a String, Symbol, Hash, Regexp, or nil"
-
end
-
end
-
end
-
-
2
class TestRequest < ActionDispatch::TestRequest #:nodoc:
-
2
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
-
2
DEFAULT_ENV.delete 'PATH_INFO'
-
-
2
def initialize(env = {})
-
29
super
-
-
29
self.session = TestSession.new
-
29
self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => SecureRandom.hex(16))
-
end
-
-
2
def assign_parameters(routes, controller_path, action, parameters = {})
-
29
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
-
29
extra_keys = routes.extra_keys(parameters)
-
29
non_path_parameters = get? ? query_parameters : request_parameters
-
29
parameters.each do |key, value|
-
87
if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?))
-
value = value.map{ |v| v.duplicable? ? v.dup : v }
-
40
elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? })
-
value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }]
-
elsif value.frozen? && value.duplicable?
-
value = value.dup
-
end
-
-
87
if extra_keys.include?(key)
-
14
non_path_parameters[key] = value
-
else
-
73
if value.is_a?(Array)
-
value = value.map(&:to_param)
-
else
-
73
value = value.to_param
-
end
-
-
73
path_parameters[key] = value
-
end
-
end
-
-
# Clear the combined params hash in case it was already referenced.
-
29
@env.delete("action_dispatch.request.parameters")
-
-
# Clear the filter cache variables so they're not stale
-
29
@filtered_parameters = @filtered_env = @filtered_path = nil
-
-
29
params = self.request_parameters.dup
-
29
%w(controller action only_path).each do |k|
-
87
params.delete(k)
-
87
params.delete(k.to_sym)
-
end
-
29
data = params.to_query
-
-
29
@env['CONTENT_LENGTH'] = data.length.to_s
-
29
@env['rack.input'] = StringIO.new(data)
-
end
-
-
2
def recycle!
-
29
@formats = nil
-
1122
@env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
-
1106
@env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
-
29
@method = @request_method = nil
-
29
@fullpath = @ip = @remote_ip = @protocol = nil
-
29
@env['action_dispatch.request.query_parameters'] = {}
-
29
@set_cookies ||= {}
-
31
@set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }])
-
29
deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies")
-
31
@set_cookies.reject!{ |k,v| deleted_cookies.include?(k) }
-
29
cookie_jar.update(rack_cookies)
-
29
cookie_jar.update(cookies)
-
29
cookie_jar.update(@set_cookies)
-
29
cookie_jar.recycle!
-
end
-
-
2
private
-
-
2
def default_env
-
29
DEFAULT_ENV
-
end
-
end
-
-
2
class TestResponse < ActionDispatch::TestResponse
-
2
def recycle!
-
29
initialize
-
end
-
end
-
-
2
class LiveTestResponse < Live::Response
-
2
def recycle!
-
@body = nil
-
initialize
-
end
-
-
2
def body
-
@body ||= super
-
end
-
-
# Was the response successful?
-
2
alias_method :success?, :successful?
-
-
# Was the URL not found?
-
2
alias_method :missing?, :not_found?
-
-
# Were we redirected?
-
2
alias_method :redirect?, :redirection?
-
-
# Was there a server-side error?
-
2
alias_method :error?, :server_error?
-
end
-
-
# Methods #destroy and #load! are overridden to avoid calling methods on the
-
# @store object, which does not exist for the TestSession class.
-
2
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
-
2
DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
-
-
2
def initialize(session = {})
-
29
super(nil, nil)
-
29
@id = SecureRandom.hex(16)
-
29
@data = stringify_keys(session)
-
29
@loaded = true
-
end
-
-
2
def exists?
-
true
-
end
-
-
2
def keys
-
@data.keys
-
end
-
-
2
def values
-
@data.values
-
end
-
-
2
def destroy
-
clear
-
end
-
-
2
def fetch(key, *args, &block)
-
@data.fetch(key.to_s, *args, &block)
-
end
-
-
2
private
-
-
2
def load!
-
@id
-
end
-
end
-
-
# Superclass for ActionController functional tests. Functional tests allow you to
-
# test a single controller action per test method. This should not be confused with
-
# integration tests (see ActionDispatch::IntegrationTest), which are more like
-
# "stories" that can involve multiple controllers and multiple actions (i.e. multiple
-
# different HTTP requests).
-
#
-
# == Basic example
-
#
-
# Functional tests are written as follows:
-
# 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate
-
# an HTTP request.
-
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
-
# the controller's HTTP response, the database contents, etc.
-
#
-
# For example:
-
#
-
# class BooksControllerTest < ActionController::TestCase
-
# def test_create
-
# # Simulate a POST response with the given HTTP parameters.
-
# post(:create, book: { title: "Love Hina" })
-
#
-
# # Assert that the controller tried to redirect us to
-
# # the created book's URI.
-
# assert_response :found
-
#
-
# # Assert that the controller really put the book in the database.
-
# assert_not_nil Book.find_by(title: "Love Hina")
-
# end
-
# end
-
#
-
# You can also send a real document in the simulated HTTP request.
-
#
-
# def test_create
-
# json = {book: { title: "Love Hina" }}.to_json
-
# post :create, json
-
# end
-
#
-
# == Special instance variables
-
#
-
# ActionController::TestCase will also automatically provide the following instance
-
# variables for use in the tests:
-
#
-
# <b>@controller</b>::
-
# The controller instance that will be tested.
-
# <b>@request</b>::
-
# An ActionController::TestRequest, representing the current HTTP
-
# request. You can modify this object before sending the HTTP request. For example,
-
# you might want to set some session properties before sending a GET request.
-
# <b>@response</b>::
-
# An ActionController::TestResponse object, representing the response
-
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
-
# after calling +post+. If the various assert methods are not sufficient, then you
-
# may use this object to inspect the HTTP response in detail.
-
#
-
# (Earlier versions of \Rails required each functional test to subclass
-
# Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
-
#
-
# == Controller is automatically inferred
-
#
-
# ActionController::TestCase will automatically infer the controller under test
-
# from the test class name. If the controller cannot be inferred from the test
-
# class name, you can explicitly set it with +tests+.
-
#
-
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
-
# tests WidgetController
-
# end
-
#
-
# == \Testing controller internals
-
#
-
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
-
# can be used against. These collections are:
-
#
-
# * assigns: Instance variables assigned in the action that are available for the view.
-
# * session: Objects being saved in the session.
-
# * flash: The flash objects currently in the session.
-
# * cookies: \Cookies being sent to the user on this request.
-
#
-
# These collections can be used just like any other hash:
-
#
-
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
-
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
-
# assert flash.empty? # makes sure that there's nothing in the flash
-
#
-
# For historic reasons, the assigns hash uses string-based keys. So <tt>assigns[:person]</tt> won't work, but <tt>assigns["person"]</tt> will. To
-
# appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing.
-
# So <tt>assigns(:person)</tt> will work just like <tt>assigns["person"]</tt>, but again, <tt>assigns[:person]</tt> will not work.
-
#
-
# On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>.
-
#
-
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
-
# action call which can then be asserted against.
-
#
-
# == Manipulating session and cookie variables
-
#
-
# Sometimes you need to set up the session and cookie variables for a test.
-
# To do this just assign a value to the session or cookie collection:
-
#
-
# session[:key] = "value"
-
# cookies[:key] = "value"
-
#
-
# To clear the cookies for a test just clear the cookie collection:
-
#
-
# cookies.clear
-
#
-
# == \Testing named routes
-
#
-
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
-
#
-
# assert_redirected_to page_url(title: 'foo')
-
2
class TestCase < ActiveSupport::TestCase
-
2
module Behavior
-
2
extend ActiveSupport::Concern
-
2
include ActionDispatch::TestProcess
-
2
include ActiveSupport::Testing::ConstantLookup
-
2
include Rails::Dom::Testing::Assertions
-
-
2
attr_reader :response, :request
-
-
2
module ClassMethods
-
-
# Sets the controller class name. Useful if the name can't be inferred from test class.
-
# Normalizes +controller_class+ before using.
-
#
-
# tests WidgetController
-
# tests :widget
-
# tests 'widget'
-
2
def tests(controller_class)
-
case controller_class
-
when String, Symbol
-
self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize
-
when Class
-
self.controller_class = controller_class
-
else
-
raise ArgumentError, "controller class must be a String, Symbol, or Class"
-
end
-
end
-
-
2
def controller_class=(new_class)
-
5
self._controller_class = new_class
-
end
-
-
2
def controller_class
-
29
if current_controller_class = self._controller_class
-
24
current_controller_class
-
else
-
5
self.controller_class = determine_default_controller_class(name)
-
end
-
end
-
-
2
def determine_default_controller_class(name)
-
5
determine_constant_from_test_name(name) do |constant|
-
5
Class === constant && constant < ActionController::Metal
-
end
-
end
-
end
-
-
# Simulate a GET request with the given parameters.
-
#
-
# - +action+: The controller action to call.
-
# - +parameters+: The HTTP parameters that you want to pass. This may
-
# be +nil+, a hash, or a string that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
-
# - +session+: A hash of parameters to store in the session. This may be +nil+.
-
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
-
#
-
# You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
-
# +post+, +patch+, +put+, +delete+, and +head+.
-
#
-
# Note that the request method is not verified. The different methods are
-
# available to make the tests more expressive.
-
2
def get(action, *args)
-
12
process(action, "GET", *args)
-
end
-
-
# Simulate a POST request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
2
def post(action, *args)
-
8
process(action, "POST", *args)
-
end
-
-
# Simulate a PATCH request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
2
def patch(action, *args)
-
6
process(action, "PATCH", *args)
-
end
-
-
# Simulate a PUT request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
2
def put(action, *args)
-
process(action, "PUT", *args)
-
end
-
-
# Simulate a DELETE request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
2
def delete(action, *args)
-
3
process(action, "DELETE", *args)
-
end
-
-
# Simulate a HEAD request with the given parameters and set/volley the response.
-
# See +get+ for more details.
-
2
def head(action, *args)
-
process(action, "HEAD", *args)
-
end
-
-
2
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
-
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
-
@request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
-
__send__(request_method, action, parameters, session, flash).tap do
-
@request.env.delete 'HTTP_X_REQUESTED_WITH'
-
@request.env.delete 'HTTP_ACCEPT'
-
end
-
end
-
2
alias xhr :xml_http_request
-
-
2
def paramify_values(hash_or_array_or_value)
-
98
case hash_or_array_or_value
-
when Hash
-
112
Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }]
-
when Array
-
hash_or_array_or_value.map {|i| paramify_values(i)}
-
when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile
-
hash_or_array_or_value
-
else
-
55
hash_or_array_or_value.to_param
-
end
-
end
-
-
# Simulate a HTTP request to +action+ by specifying request method,
-
# parameters and set/volley the response.
-
#
-
# - +action+: The controller action to call.
-
# - +http_method+: Request method used to send the http request. Possible values
-
# are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+.
-
# - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a
-
# string that is appropriately encoded (+application/x-www-form-urlencoded+
-
# or +multipart/form-data+).
-
# - +session+: A hash of parameters to store in the session. This may be +nil+.
-
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
-
#
-
# Example calling +create+ action and sending two params:
-
#
-
# process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' }
-
#
-
# Example sending parameters, +nil+ session and setting a flash message:
-
#
-
# process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' }
-
#
-
# To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests
-
# prefer using #get, #post, #patch, #put, #delete and #head methods
-
# respectively which will make tests more expressive.
-
#
-
# Note that the request method is not verified.
-
2
def process(action, http_method = 'GET', *args)
-
29
check_required_ivars
-
-
29
if args.first.is_a?(String) && http_method != 'HEAD'
-
@request.env['RAW_POST_DATA'] = args.shift
-
end
-
-
29
parameters, session, flash = args
-
29
parameters ||= {}
-
-
# Ensure that numbers and symbols passed as params are converted to
-
# proper params, as is the case when engaging rack.
-
29
parameters = paramify_values(parameters) if html_format?(parameters)
-
-
29
@html_document = nil
-
29
@html_scanner_document = nil
-
-
29
unless @controller.respond_to?(:recycle!)
-
27
@controller.extend(Testing::Functional)
-
end
-
-
29
@request.recycle!
-
29
@response.recycle!
-
29
@controller.recycle!
-
-
29
@request.env['REQUEST_METHOD'] = http_method
-
-
29
controller_class_name = @controller.class.anonymous? ?
-
"anonymous" :
-
@controller.class.controller_path
-
-
29
@request.assign_parameters(@routes, controller_class_name, action.to_s, parameters)
-
-
29
@request.session.update(session) if session
-
29
@request.flash.update(flash || {})
-
-
29
@controller.request = @request
-
29
@controller.response = @response
-
-
29
build_request_uri(action, parameters)
-
-
29
name = @request.parameters[:action]
-
-
29
@controller.recycle!
-
29
@controller.process(name)
-
-
29
if cookies = @request.env['action_dispatch.cookies']
-
29
unless @response.committed?
-
29
cookies.write(@response)
-
end
-
end
-
29
@response.prepare!
-
-
29
@assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {}
-
-
29
if flash_value = @request.flash.to_session_value
-
13
@request.session['flash'] = flash_value
-
end
-
-
29
@response
-
end
-
-
2
def setup_controller_request_and_response
-
29
@controller = nil unless defined? @controller
-
-
29
response_klass = TestResponse
-
-
29
if klass = self.class.controller_class
-
29
if klass < ActionController::Live
-
response_klass = LiveTestResponse
-
end
-
29
unless @controller
-
29
begin
-
29
@controller = klass.new
-
rescue
-
warn "could not construct controller #{klass}" if $VERBOSE
-
end
-
end
-
end
-
-
29
@request = build_request
-
29
@response = build_response response_klass
-
29
@response.request = @request
-
-
29
if @controller
-
29
@controller.request = @request
-
29
@controller.params = {}
-
end
-
end
-
-
2
def build_request
-
29
TestRequest.new
-
end
-
-
2
def build_response(klass)
-
29
klass.new
-
end
-
-
2
included do
-
2
include ActionController::TemplateAssertions
-
2
include ActionDispatch::Assertions
-
2
class_attribute :_controller_class
-
2
setup :setup_controller_request_and_response
-
end
-
-
2
private
-
-
2
def document_root_element
-
html_document.root
-
end
-
-
2
def check_required_ivars
-
# Sanity check for required instance variables so we can give an
-
# understandable error message.
-
29
[:@routes, :@controller, :@request, :@response].each do |iv_name|
-
116
if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
-
raise "#{iv_name} is nil: make sure you set it in your test's setup method."
-
end
-
end
-
end
-
-
2
def build_request_uri(action, parameters)
-
29
unless @request.env["PATH_INFO"]
-
27
options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters
-
27
options.update(
-
:action => action,
-
:relative_url_root => nil,
-
:_recall => @request.path_parameters)
-
-
27
if route_name = options.delete(:use_route)
-
ActiveSupport::Deprecation.warn <<-MSG.squish
-
Passing the `use_route` option in functional tests are deprecated.
-
Support for this option in the `process` method (and the related
-
`get`, `head`, `post`, `patch`, `put` and `delete` helpers) will
-
be removed in the next version without replacement.
-
-
Functional tests are essentially unit tests for controllers and
-
they should not require knowledge to how the application's routes
-
are configured. Instead, you should explicitly pass the appropiate
-
params to the `process` method.
-
-
Previously the engines guide also contained an incorrect example
-
that recommended using this option to test an engine's controllers
-
within the dummy application. That recommendation was incorrect
-
and has since been corrected. Instead, you should override the
-
`@routes` variable in the test case with `Foo::Engine.routes`. See
-
the updated engines guide for details.
-
MSG
-
end
-
-
27
url, query_string = @routes.path_for(options, route_name).split("?", 2)
-
-
27
@request.env["SCRIPT_NAME"] = @controller.config.relative_url_root
-
27
@request.env["PATH_INFO"] = url
-
27
@request.env["QUERY_STRING"] = query_string || ""
-
end
-
end
-
-
2
def html_format?(parameters)
-
29
return true unless parameters.key?(:format)
-
Mime.fetch(parameters[:format]) { Mime['html'] }.html?
-
end
-
end
-
-
2
include Behavior
-
end
-
end
-
-
2
module ActionDispatch
-
# Provides callbacks to be executed before and after dispatching the request.
-
2
class Callbacks
-
2
include ActiveSupport::Callbacks
-
-
2
define_callbacks :call
-
-
2
class << self
-
2
delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
-
-
2
def before(*args, &block)
-
set_callback(:call, :before, *args, &block)
-
end
-
-
2
def after(*args, &block)
-
set_callback(:call, :after, *args, &block)
-
end
-
end
-
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
error = nil
-
result = run_callbacks :call do
-
begin
-
@app.call(env)
-
rescue => error
-
end
-
end
-
raise error if error
-
result
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/keys'
-
2
require 'active_support/core_ext/module/attribute_accessors'
-
2
require 'active_support/core_ext/object/blank'
-
2
require 'active_support/key_generator'
-
2
require 'active_support/message_verifier'
-
2
require 'active_support/json'
-
-
2
module ActionDispatch
-
2
class Request < Rack::Request
-
2
def cookie_jar
-
203
env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
-
end
-
end
-
-
# \Cookies are read and written through ActionController#cookies.
-
#
-
# The cookies being read are the ones received along with the request, the cookies
-
# being written will be sent out with the response. Reading a cookie does not get
-
# the cookie object itself back, just the value it holds.
-
#
-
# Examples of writing:
-
#
-
# # Sets a simple session cookie.
-
# # This cookie will be deleted when the user's browser is closed.
-
# cookies[:user_name] = "david"
-
#
-
# # Cookie values are String based. Other data types need to be serialized.
-
# cookies[:lat_lon] = JSON.generate([47.68, -122.37])
-
#
-
# # Sets a cookie that expires in 1 hour.
-
# cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
-
#
-
# # Sets a signed cookie, which prevents users from tampering with its value.
-
# # The cookie is signed by your app's `secrets.secret_key_base` value.
-
# # It can be read using the signed method `cookies.signed[:name]`
-
# cookies.signed[:user_id] = current_user.id
-
#
-
# # Sets a "permanent" cookie (which expires in 20 years from now).
-
# cookies.permanent[:login] = "XJ-122"
-
#
-
# # You can also chain these methods:
-
# cookies.permanent.signed[:login] = "XJ-122"
-
#
-
# Examples of reading:
-
#
-
# cookies[:user_name] # => "david"
-
# cookies.size # => 2
-
# JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
-
# cookies.signed[:login] # => "XJ-122"
-
#
-
# Example for deleting:
-
#
-
# cookies.delete :user_name
-
#
-
# Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
-
#
-
# cookies[:name] = {
-
# value: 'a yummy cookie',
-
# expires: 1.year.from_now,
-
# domain: 'domain.com'
-
# }
-
#
-
# cookies.delete(:name, domain: 'domain.com')
-
#
-
# The option symbols for setting cookies are:
-
#
-
# * <tt>:value</tt> - The cookie's value.
-
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
-
# of the application.
-
# * <tt>:domain</tt> - The domain for which this cookie applies so you can
-
# restrict to the domain level. If you use a schema like www.example.com
-
# and want to share session with user.example.com set <tt>:domain</tt>
-
# to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
-
# <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
-
#
-
# domain: nil # Does not sets cookie domain. (default)
-
# domain: :all # Allow the cookie for the top most level
-
# # domain and subdomains.
-
# domain: %w(.example.com .example.org) # Allow the cookie
-
# # for concrete domain names.
-
#
-
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
-
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
-
# Default is +false+.
-
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
-
# only HTTP. Defaults to +false+.
-
2
class Cookies
-
2
HTTP_HEADER = "Set-Cookie".freeze
-
2
GENERATOR_KEY = "action_dispatch.key_generator".freeze
-
2
SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
-
2
ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
-
2
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
-
2
SECRET_TOKEN = "action_dispatch.secret_token".freeze
-
2
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
-
2
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
-
2
COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
-
-
# Cookies can typically store 4096 bytes.
-
2
MAX_COOKIE_SIZE = 4096
-
-
# Raised when storing more than 4K of session data.
-
2
CookieOverflow = Class.new StandardError
-
-
# Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
-
2
module ChainedCookieJars
-
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
-
#
-
# cookies.permanent[:prefers_open_id] = true
-
# # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
-
#
-
# This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
-
#
-
# This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
-
#
-
# cookies.permanent.signed[:remember_me] = current_user.id
-
# # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
-
2
def permanent
-
@permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
-
end
-
-
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
-
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
-
# cookie was tampered with by the user (or a 3rd party), nil will be returned.
-
#
-
# If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
-
# legacy cookies signed with the old key generator will be transparently upgraded.
-
#
-
# This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
-
#
-
# Example:
-
#
-
# cookies.signed[:discount] = 45
-
# # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
-
#
-
# cookies.signed[:discount] # => 45
-
2
def signed
-
@signed ||=
-
if @options[:upgrade_legacy_signed_cookies]
-
UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
-
else
-
SignedCookieJar.new(self, @key_generator, @options)
-
end
-
end
-
-
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
-
# If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
-
#
-
# If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
-
# legacy cookies signed with the old key generator will be transparently upgraded.
-
#
-
# This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
-
#
-
# Example:
-
#
-
# cookies.encrypted[:discount] = 45
-
# # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
-
#
-
# cookies.encrypted[:discount] # => 45
-
2
def encrypted
-
@encrypted ||=
-
if @options[:upgrade_legacy_signed_cookies]
-
UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
-
else
-
EncryptedCookieJar.new(self, @key_generator, @options)
-
end
-
end
-
-
# Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
-
# Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
-
2
def signed_or_encrypted
-
@signed_or_encrypted ||=
-
if @options[:secret_key_base].present?
-
encrypted
-
else
-
signed
-
end
-
end
-
end
-
-
# Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
-
# to the Message{Encryptor,Verifier} allows us to handle the
-
# (de)serialization step within the cookie jar, which gives us the
-
# opportunity to detect and migrate legacy cookies.
-
2
module VerifyAndUpgradeLegacySignedMessage # :nodoc:
-
2
def initialize(*args)
-
super
-
@legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
-
end
-
-
2
def verify_and_upgrade_legacy_signed_message(name, signed_message)
-
deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
-
self[name] = { value: value }
-
end
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
-
nil
-
end
-
end
-
-
2
class CookieJar #:nodoc:
-
2
include Enumerable, ChainedCookieJars
-
-
# This regular expression is used to split the levels of a domain.
-
# The top level domain can be any string without a period or
-
# **.**, ***.** style TLDs like co.uk or com.au
-
#
-
# www.example.co.uk gives:
-
# $& => example.co.uk
-
#
-
# example.com gives:
-
# $& => example.com
-
#
-
# lots.of.subdomains.example.local gives:
-
# $& => example.local
-
2
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
-
-
2
def self.options_for_env(env) #:nodoc:
-
{ signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
-
encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
-
encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
-
secret_token: env[SECRET_TOKEN],
-
secret_key_base: env[SECRET_KEY_BASE],
-
upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
-
serializer: env[COOKIES_SERIALIZER],
-
digest: env[COOKIES_DIGEST]
-
27
}
-
end
-
-
2
def self.build(request)
-
27
env = request.env
-
27
key_generator = env[GENERATOR_KEY]
-
27
options = options_for_env env
-
-
27
host = request.host
-
27
secure = request.ssl?
-
-
27
new(key_generator, host, secure, options).tap do |hash|
-
27
hash.update(request.cookies)
-
end
-
end
-
-
2
def initialize(key_generator, host = nil, secure = false, options = {})
-
27
@key_generator = key_generator
-
27
@set_cookies = {}
-
27
@delete_cookies = {}
-
27
@host = host
-
27
@secure = secure
-
27
@options = options
-
27
@cookies = {}
-
27
@committed = false
-
end
-
-
2
def committed?; @committed; end
-
-
2
def commit!
-
@committed = true
-
@set_cookies.freeze
-
@delete_cookies.freeze
-
end
-
-
2
def each(&block)
-
@cookies.each(&block)
-
end
-
-
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
-
2
def [](name)
-
@cookies[name.to_s]
-
end
-
-
2
def fetch(name, *args, &block)
-
@cookies.fetch(name.to_s, *args, &block)
-
end
-
-
2
def key?(name)
-
@cookies.key?(name.to_s)
-
end
-
2
alias :has_key? :key?
-
-
2
def update(other_hash)
-
114
@cookies.update other_hash.stringify_keys
-
114
self
-
end
-
-
2
def handle_options(options) #:nodoc:
-
17
options[:path] ||= "/"
-
-
17
if options[:domain] == :all
-
# if there is a provided tld length then we use it otherwise default domain regexp
-
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
-
-
# if host is not ip and matches domain regexp
-
# (ip confirms to domain regexp so we explicitly check for ip)
-
options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp)
-
".#{$&}"
-
end
-
17
elsif options[:domain].is_a? Array
-
# if host matches one of the supplied domains without a dot in front of it
-
options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') }
-
end
-
end
-
-
# Sets the cookie named +name+. The second argument may be the cookie's
-
# value or a hash of options as documented above.
-
2
def []=(name, options)
-
17
if options.is_a?(Hash)
-
options.symbolize_keys!
-
value = options[:value]
-
else
-
17
value = options
-
17
options = { :value => value }
-
end
-
-
17
handle_options(options)
-
-
17
if @cookies[name.to_s] != value or options[:expires]
-
16
@cookies[name.to_s] = value
-
16
@set_cookies[name.to_s] = options
-
16
@delete_cookies.delete(name.to_s)
-
end
-
-
17
value
-
end
-
-
# Removes the cookie on the client machine by setting the value to an empty string
-
# and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
-
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
-
2
def delete(name, options = {})
-
12
return unless @cookies.has_key? name.to_s
-
-
options.symbolize_keys!
-
handle_options(options)
-
-
value = @cookies.delete(name.to_s)
-
@delete_cookies[name.to_s] = options
-
value
-
end
-
-
# Whether the given cookie is to be deleted by this CookieJar.
-
# Like <tt>[]=</tt>, you can pass in an options hash to test if a
-
# deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
-
2
def deleted?(name, options = {})
-
options.symbolize_keys!
-
handle_options(options)
-
@delete_cookies[name.to_s] == options
-
end
-
-
# Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
-
2
def clear(options = {})
-
@cookies.each_key{ |k| delete(k, options) }
-
end
-
-
2
def write(headers)
-
45
@set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
-
29
@delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
-
end
-
-
2
def recycle! #:nodoc:
-
29
@set_cookies = {}
-
29
@delete_cookies = {}
-
end
-
-
2
mattr_accessor :always_write_cookie
-
2
self.always_write_cookie = false
-
-
2
private
-
2
def write_cookie?(cookie)
-
16
@secure || !cookie[:secure] || always_write_cookie
-
end
-
end
-
-
2
class PermanentCookieJar #:nodoc:
-
2
include ChainedCookieJars
-
-
2
def initialize(parent_jar, key_generator, options = {})
-
@parent_jar = parent_jar
-
@key_generator = key_generator
-
@options = options
-
end
-
-
2
def [](name)
-
@parent_jar[name.to_s]
-
end
-
-
2
def []=(name, options)
-
if options.is_a?(Hash)
-
options.symbolize_keys!
-
else
-
options = { :value => options }
-
end
-
-
options[:expires] = 20.years.from_now
-
@parent_jar[name] = options
-
end
-
end
-
-
2
class JsonSerializer # :nodoc:
-
2
def self.load(value)
-
ActiveSupport::JSON.decode(value)
-
end
-
-
2
def self.dump(value)
-
ActiveSupport::JSON.encode(value)
-
end
-
end
-
-
2
module SerializedCookieJars # :nodoc:
-
2
MARSHAL_SIGNATURE = "\x04\x08".freeze
-
-
2
protected
-
2
def needs_migration?(value)
-
@options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
-
end
-
-
2
def serialize(name, value)
-
serializer.dump(value)
-
end
-
-
2
def deserialize(name, value)
-
if value
-
if needs_migration?(value)
-
Marshal.load(value).tap do |v|
-
self[name] = { value: v }
-
end
-
else
-
serializer.load(value)
-
end
-
end
-
end
-
-
2
def serializer
-
serializer = @options[:serializer] || :marshal
-
case serializer
-
when :marshal
-
Marshal
-
when :json, :hybrid
-
JsonSerializer
-
else
-
serializer
-
end
-
end
-
-
2
def digest
-
@options[:digest] || 'SHA1'
-
end
-
end
-
-
2
class SignedCookieJar #:nodoc:
-
2
include ChainedCookieJars
-
2
include SerializedCookieJars
-
-
2
def initialize(parent_jar, key_generator, options = {})
-
@parent_jar = parent_jar
-
@options = options
-
secret = key_generator.generate_key(@options[:signed_cookie_salt])
-
@verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
-
end
-
-
2
def [](name)
-
if signed_message = @parent_jar[name]
-
deserialize name, verify(signed_message)
-
end
-
end
-
-
2
def []=(name, options)
-
if options.is_a?(Hash)
-
options.symbolize_keys!
-
options[:value] = @verifier.generate(serialize(name, options[:value]))
-
else
-
options = { :value => @verifier.generate(serialize(name, options)) }
-
end
-
-
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
-
@parent_jar[name] = options
-
end
-
-
2
private
-
2
def verify(signed_message)
-
@verifier.verify(signed_message)
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
-
nil
-
end
-
end
-
-
# UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
-
# secrets.secret_token and secrets.secret_key_base are both set. It reads
-
# legacy cookies signed with the old dummy key generator and re-saves
-
# them using the new key generator to provide a smooth upgrade path.
-
2
class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
-
2
include VerifyAndUpgradeLegacySignedMessage
-
-
2
def [](name)
-
if signed_message = @parent_jar[name]
-
deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
-
end
-
end
-
end
-
-
2
class EncryptedCookieJar #:nodoc:
-
2
include ChainedCookieJars
-
2
include SerializedCookieJars
-
-
2
def initialize(parent_jar, key_generator, options = {})
-
if ActiveSupport::LegacyKeyGenerator === key_generator
-
raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
-
"Read the upgrade documentation to learn more about this new config option."
-
end
-
-
@parent_jar = parent_jar
-
@options = options
-
secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
-
sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
-
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
-
end
-
-
2
def [](name)
-
if encrypted_message = @parent_jar[name]
-
deserialize name, decrypt_and_verify(encrypted_message)
-
end
-
end
-
-
2
def []=(name, options)
-
if options.is_a?(Hash)
-
options.symbolize_keys!
-
else
-
options = { :value => options }
-
end
-
-
options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
-
-
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
-
@parent_jar[name] = options
-
end
-
-
2
private
-
2
def decrypt_and_verify(encrypted_message)
-
@encryptor.decrypt_and_verify(encrypted_message)
-
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
-
nil
-
end
-
end
-
-
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
-
# instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base
-
# are both set. It reads legacy cookies signed with the old dummy key generator and
-
# encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
-
2
class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
-
2
include VerifyAndUpgradeLegacySignedMessage
-
-
2
def [](name)
-
if encrypted_or_signed_message = @parent_jar[name]
-
deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
-
end
-
end
-
end
-
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
status, headers, body = @app.call(env)
-
-
if cookie_jar = env['action_dispatch.cookies']
-
unless cookie_jar.committed?
-
cookie_jar.write(headers)
-
if headers[HTTP_HEADER].respond_to?(:join)
-
headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
-
end
-
end
-
end
-
-
[status, headers, body]
-
end
-
end
-
end
-
2
require 'action_dispatch/http/request'
-
2
require 'action_dispatch/middleware/exception_wrapper'
-
2
require 'action_dispatch/routing/inspector'
-
-
2
module ActionDispatch
-
# This middleware is responsible for logging exceptions and
-
# showing a debugging page in case the request is local.
-
2
class DebugExceptions
-
2
RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__)
-
-
2
def initialize(app, routes_app = nil)
-
2
@app = app
-
2
@routes_app = routes_app
-
end
-
-
2
def call(env)
-
_, headers, body = response = @app.call(env)
-
-
if headers['X-Cascade'] == 'pass'
-
body.close if body.respond_to?(:close)
-
raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
-
end
-
-
response
-
rescue Exception => exception
-
raise exception if env['action_dispatch.show_exceptions'] == false
-
render_exception(env, exception)
-
end
-
-
2
private
-
-
2
def render_exception(env, exception)
-
wrapper = ExceptionWrapper.new(env, exception)
-
log_error(env, wrapper)
-
-
if env['action_dispatch.show_detailed_exceptions']
-
request = Request.new(env)
-
traces = wrapper.traces
-
-
trace_to_show = 'Application Trace'
-
if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
-
trace_to_show = 'Full Trace'
-
end
-
-
if source_to_show = traces[trace_to_show].first
-
source_to_show_id = source_to_show[:id]
-
end
-
-
template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
-
request: request,
-
exception: wrapper.exception,
-
traces: traces,
-
show_source_idx: source_to_show_id,
-
trace_to_show: trace_to_show,
-
routes_inspector: routes_inspector(exception),
-
source_extracts: wrapper.source_extracts,
-
line_number: wrapper.line_number,
-
file: wrapper.file
-
)
-
file = "rescues/#{wrapper.rescue_template}"
-
-
if request.xhr?
-
body = template.render(template: file, layout: false, formats: [:text])
-
format = "text/plain"
-
else
-
body = template.render(template: file, layout: 'rescues/layout')
-
format = "text/html"
-
end
-
render(wrapper.status_code, body, format)
-
else
-
raise exception
-
end
-
end
-
-
2
def render(status, body, format)
-
[status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
-
end
-
-
2
def log_error(env, wrapper)
-
logger = logger(env)
-
return unless logger
-
-
exception = wrapper.exception
-
-
trace = wrapper.application_trace
-
trace = wrapper.framework_trace if trace.empty?
-
-
ActiveSupport::Deprecation.silence do
-
message = "\n#{exception.class} (#{exception.message}):\n"
-
message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
-
message << " " << trace.join("\n ")
-
logger.fatal("#{message}\n\n")
-
end
-
end
-
-
2
def logger(env)
-
env['action_dispatch.logger'] || stderr_logger
-
end
-
-
2
def stderr_logger
-
@stderr_logger ||= ActiveSupport::Logger.new($stderr)
-
end
-
-
2
def routes_inspector(exception)
-
if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
-
ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
-
end
-
end
-
end
-
end
-
2
require 'action_controller/metal/exceptions'
-
2
require 'active_support/core_ext/module/attribute_accessors'
-
-
2
module ActionDispatch
-
2
class ExceptionWrapper
-
2
cattr_accessor :rescue_responses
-
2
@@rescue_responses = Hash.new(:internal_server_error)
-
2
@@rescue_responses.merge!(
-
'ActionController::RoutingError' => :not_found,
-
'AbstractController::ActionNotFound' => :not_found,
-
'ActionController::MethodNotAllowed' => :method_not_allowed,
-
'ActionController::UnknownHttpMethod' => :method_not_allowed,
-
'ActionController::NotImplemented' => :not_implemented,
-
'ActionController::UnknownFormat' => :not_acceptable,
-
'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
-
'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
-
'ActionDispatch::ParamsParser::ParseError' => :bad_request,
-
'ActionController::BadRequest' => :bad_request,
-
'ActionController::ParameterMissing' => :bad_request
-
)
-
-
2
cattr_accessor :rescue_templates
-
2
@@rescue_templates = Hash.new('diagnostics')
-
2
@@rescue_templates.merge!(
-
'ActionView::MissingTemplate' => 'missing_template',
-
'ActionController::RoutingError' => 'routing_error',
-
'AbstractController::ActionNotFound' => 'unknown_action',
-
'ActionView::Template::Error' => 'template_error'
-
)
-
-
2
attr_reader :env, :exception, :line_number, :file
-
-
2
def initialize(env, exception)
-
@env = env
-
@exception = original_exception(exception)
-
-
expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError)
-
end
-
-
2
def rescue_template
-
@@rescue_templates[@exception.class.name]
-
end
-
-
2
def status_code
-
self.class.status_code_for_exception(@exception.class.name)
-
end
-
-
2
def application_trace
-
clean_backtrace(:silent)
-
end
-
-
2
def framework_trace
-
clean_backtrace(:noise)
-
end
-
-
2
def full_trace
-
clean_backtrace(:all)
-
end
-
-
2
def traces
-
appplication_trace_with_ids = []
-
framework_trace_with_ids = []
-
full_trace_with_ids = []
-
-
full_trace.each_with_index do |trace, idx|
-
trace_with_id = { id: idx, trace: trace }
-
-
if application_trace.include?(trace)
-
appplication_trace_with_ids << trace_with_id
-
else
-
framework_trace_with_ids << trace_with_id
-
end
-
-
full_trace_with_ids << trace_with_id
-
end
-
-
{
-
"Application Trace" => appplication_trace_with_ids,
-
"Framework Trace" => framework_trace_with_ids,
-
"Full Trace" => full_trace_with_ids
-
}
-
end
-
-
2
def self.status_code_for_exception(class_name)
-
Rack::Utils.status_code(@@rescue_responses[class_name])
-
end
-
-
2
def source_extracts
-
backtrace.map do |trace|
-
file, line = trace.split(":")
-
line_number = line.to_i
-
-
{
-
code: source_fragment(file, line_number),
-
line_number: line_number
-
}
-
end
-
end
-
-
2
private
-
-
2
def backtrace
-
Array(@exception.backtrace)
-
end
-
-
2
def original_exception(exception)
-
if registered_original_exception?(exception)
-
exception.original_exception
-
else
-
exception
-
end
-
end
-
-
2
def registered_original_exception?(exception)
-
exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name)
-
end
-
-
2
def clean_backtrace(*args)
-
if backtrace_cleaner
-
backtrace_cleaner.clean(backtrace, *args)
-
else
-
backtrace
-
end
-
end
-
-
2
def backtrace_cleaner
-
@backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner']
-
end
-
-
2
def source_fragment(path, line)
-
return unless Rails.respond_to?(:root) && Rails.root
-
full_path = Rails.root.join(path)
-
if File.exist?(full_path)
-
File.open(full_path, "r") do |file|
-
start = [line - 3, 0].max
-
lines = file.each_line.drop(start).take(6)
-
Hash[*(start+1..(lines.count+start)).zip(lines).flatten]
-
end
-
end
-
end
-
-
2
def expand_backtrace
-
@exception.backtrace.unshift(
-
@exception.to_s.split("\n")
-
).flatten!
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/keys'
-
-
2
module ActionDispatch
-
2
class Request < Rack::Request
-
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
-
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
-
# to put a new one.
-
2
def flash
-
104
@env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"])
-
end
-
end
-
-
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
-
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
-
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
-
# then expose the flash to its template. Actually, that exposure is automatically done.
-
#
-
# class PostsController < ActionController::Base
-
# def create
-
# # save post
-
# flash[:notice] = "Post successfully created"
-
# redirect_to @post
-
# end
-
#
-
# def show
-
# # doesn't need to assign the flash notice to the template, that's done automatically
-
# end
-
# end
-
#
-
# show.html.erb
-
# <% if flash[:notice] %>
-
# <div class="notice"><%= flash[:notice] %></div>
-
# <% end %>
-
#
-
# Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
-
#
-
# flash.alert = "You must be logged in"
-
# flash.notice = "Post successfully created"
-
#
-
# This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
-
# non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
-
# use sanitize helper.
-
#
-
# Just remember: They'll be gone by the time the next action has been performed.
-
#
-
# See docs on the FlashHash class for more details about the flash.
-
2
class Flash
-
2
KEY = 'action_dispatch.request.flash_hash'.freeze
-
-
2
class FlashNow #:nodoc:
-
2
attr_accessor :flash
-
-
2
def initialize(flash)
-
@flash = flash
-
end
-
-
2
def []=(k, v)
-
k = k.to_s
-
@flash[k] = v
-
@flash.discard(k)
-
v
-
end
-
-
2
def [](k)
-
@flash[k.to_s]
-
end
-
-
# Convenience accessor for <tt>flash.now[:alert]=</tt>.
-
2
def alert=(message)
-
self[:alert] = message
-
end
-
-
# Convenience accessor for <tt>flash.now[:notice]=</tt>.
-
2
def notice=(message)
-
self[:notice] = message
-
end
-
end
-
-
2
class FlashHash
-
2
include Enumerable
-
-
2
def self.from_session_value(value) #:nodoc:
-
29
flash = case value
-
when FlashHash # Rails 3.1, 3.2
-
new(value.instance_variable_get(:@flashes), value.instance_variable_get(:@used))
-
when Hash # Rails 4.0
-
2
new(value['flashes'], value['discard'])
-
else
-
27
new
-
end
-
-
29
flash.tap(&:sweep)
-
end
-
-
# Builds a hash containing the discarded values and the hashes
-
# representing the flashes.
-
# If there are no values in @flashes, returns nil.
-
2
def to_session_value #:nodoc:
-
29
return nil if empty?
-
13
{'discard' => @discard.to_a, 'flashes' => @flashes}
-
end
-
-
2
def initialize(flashes = {}, discard = []) #:nodoc:
-
29
@discard = Set.new(stringify_array(discard))
-
29
@flashes = flashes.stringify_keys
-
29
@now = nil
-
end
-
-
2
def initialize_copy(other)
-
if other.now_is_loaded?
-
@now = other.now.dup
-
@now.flash = self
-
end
-
super
-
end
-
-
2
def []=(k, v)
-
13
k = k.to_s
-
13
@discard.delete k
-
13
@flashes[k] = v
-
end
-
-
2
def [](k)
-
33
@flashes[k.to_s]
-
end
-
-
2
def update(h) #:nodoc:
-
29
@discard.subtract stringify_array(h.keys)
-
29
@flashes.update h.stringify_keys
-
29
self
-
end
-
-
2
def keys
-
@flashes.keys
-
end
-
-
2
def key?(name)
-
@flashes.key? name.to_s
-
end
-
-
2
def delete(key)
-
key = key.to_s
-
@discard.delete key
-
@flashes.delete key
-
self
-
end
-
-
2
def to_hash
-
@flashes.dup
-
end
-
-
2
def empty?
-
29
@flashes.empty?
-
end
-
-
2
def clear
-
@discard.clear
-
@flashes.clear
-
end
-
-
2
def each(&block)
-
@flashes.each(&block)
-
end
-
-
2
alias :merge! :update
-
-
2
def replace(h) #:nodoc:
-
@discard.clear
-
@flashes.replace h.stringify_keys
-
self
-
end
-
-
# Sets a flash that will not be available to the next action, only to the current.
-
#
-
# flash.now[:message] = "Hello current action"
-
#
-
# This method enables you to use the flash as a central messaging system in your app.
-
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
-
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
-
# vanish when the current action is done.
-
#
-
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
-
#
-
# Also, brings two convenience accessors:
-
#
-
# flash.now.alert = "Beware now!"
-
# # Equivalent to flash.now[:alert] = "Beware now!"
-
#
-
# flash.now.notice = "Good luck now!"
-
# # Equivalent to flash.now[:notice] = "Good luck now!"
-
2
def now
-
@now ||= FlashNow.new(self)
-
end
-
-
# Keeps either the entire current flash or a specific flash entry available for the next action:
-
#
-
# flash.keep # keeps the entire flash
-
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
-
2
def keep(k = nil)
-
k = k.to_s if k
-
@discard.subtract Array(k || keys)
-
k ? self[k] : self
-
end
-
-
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
-
#
-
# flash.discard # discard the entire flash at the end of the current action
-
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
-
2
def discard(k = nil)
-
k = k.to_s if k
-
@discard.merge Array(k || keys)
-
k ? self[k] : self
-
end
-
-
# Mark for removal entries that were kept, and delete unkept ones.
-
#
-
# This method is called automatically by filters, so you generally don't need to care about it.
-
2
def sweep #:nodoc:
-
29
@discard.each { |k| @flashes.delete k }
-
29
@discard.replace @flashes.keys
-
end
-
-
# Convenience accessor for <tt>flash[:alert]</tt>.
-
2
def alert
-
self[:alert]
-
end
-
-
# Convenience accessor for <tt>flash[:alert]=</tt>.
-
2
def alert=(message)
-
self[:alert] = message
-
end
-
-
# Convenience accessor for <tt>flash[:notice]</tt>.
-
2
def notice
-
self[:notice]
-
end
-
-
# Convenience accessor for <tt>flash[:notice]=</tt>.
-
2
def notice=(message)
-
self[:notice] = message
-
end
-
-
2
protected
-
2
def now_is_loaded?
-
@now
-
end
-
-
2
def stringify_array(array)
-
58
array.map do |item|
-
item.kind_of?(Symbol) ? item.to_s : item
-
end
-
end
-
end
-
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
@app.call(env)
-
ensure
-
session = Request::Session.find(env) || {}
-
flash_hash = env[KEY]
-
-
if flash_hash && (flash_hash.present? || session.key?('flash'))
-
session["flash"] = flash_hash.to_session_value
-
env[KEY] = flash_hash.dup
-
end
-
-
if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
-
session.key?('flash') && session['flash'].nil?
-
session.delete('flash')
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/conversions'
-
2
require 'action_dispatch/http/request'
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
-
2
module ActionDispatch
-
2
class ParamsParser
-
2
class ParseError < StandardError
-
2
attr_reader :original_exception
-
-
2
def initialize(message, original_exception)
-
super(message)
-
@original_exception = original_exception
-
end
-
end
-
-
2
DEFAULT_PARSERS = { Mime::JSON => :json }
-
-
2
def initialize(app, parsers = {})
-
2
@app, @parsers = app, DEFAULT_PARSERS.merge(parsers)
-
end
-
-
2
def call(env)
-
if params = parse_formatted_parameters(env)
-
env["action_dispatch.request.request_parameters"] = params
-
end
-
-
@app.call(env)
-
end
-
-
2
private
-
2
def parse_formatted_parameters(env)
-
request = Request.new(env)
-
-
return false if request.content_length.zero?
-
-
strategy = @parsers[request.content_mime_type]
-
-
return false unless strategy
-
-
case strategy
-
when Proc
-
strategy.call(request.raw_post)
-
when :json
-
data = ActiveSupport::JSON.decode(request.raw_post)
-
data = {:_json => data} unless data.is_a?(Hash)
-
Request::Utils.deep_munge(data).with_indifferent_access
-
else
-
false
-
end
-
rescue => e # JSON or Ruby code block errors
-
logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
-
-
raise ParseError.new(e.message, e)
-
end
-
-
2
def logger(env)
-
env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr)
-
end
-
end
-
end
-
2
module ActionDispatch
-
# When called, this middleware renders an error page. By default if an HTML
-
# response is expected it will render static error pages from the `/public`
-
# directory. For example when this middleware receives a 500 response it will
-
# render the template found in `/public/500.html`.
-
# If an internationalized locale is set, this middleware will attempt to render
-
# the template in `/public/500.<locale>.html`. If an internationalized template
-
# is not found it will fall back on `/public/500.html`.
-
#
-
# When a request with a content type other than HTML is made, this middleware
-
# will attempt to convert error information into the appropriate response type.
-
2
class PublicExceptions
-
2
attr_accessor :public_path
-
-
2
def initialize(public_path)
-
2
@public_path = public_path
-
end
-
-
2
def call(env)
-
status = env["PATH_INFO"][1..-1]
-
request = ActionDispatch::Request.new(env)
-
content_type = request.formats.first
-
body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status.to_i, Rack::Utils::HTTP_STATUS_CODES[500]) }
-
-
render(status, content_type, body)
-
end
-
-
2
private
-
-
2
def render(status, content_type, body)
-
format = "to_#{content_type.to_sym}" if content_type
-
if format && body.respond_to?(format)
-
render_format(status, content_type, body.public_send(format))
-
else
-
render_html(status)
-
end
-
end
-
-
2
def render_format(status, content_type, body)
-
[status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
-
'Content-Length' => body.bytesize.to_s}, [body]]
-
end
-
-
2
def render_html(status)
-
path = "#{public_path}/#{status}.#{I18n.locale}.html"
-
path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
-
-
if found || File.exist?(path)
-
render_format(status, 'text/html', File.read(path))
-
else
-
[404, { "X-Cascade" => "pass" }, []]
-
end
-
end
-
end
-
end
-
2
require 'active_support/deprecation/reporting'
-
-
2
module ActionDispatch
-
# ActionDispatch::Reloader provides prepare and cleanup callbacks,
-
# intended to assist with code reloading during development.
-
#
-
# Prepare callbacks are run before each request, and cleanup callbacks
-
# after each request. In this respect they are analogs of ActionDispatch::Callback's
-
# before and after callbacks. However, cleanup callbacks are not called until the
-
# request is fully complete -- that is, after #close has been called on
-
# the response body. This is important for streaming responses such as the
-
# following:
-
#
-
# self.response_body = lambda { |response, output|
-
# # code here which refers to application models
-
# }
-
#
-
# Cleanup callbacks will not be called until after the response_body lambda
-
# is evaluated, ensuring that it can refer to application models and other
-
# classes before they are unloaded.
-
#
-
# By default, ActionDispatch::Reloader is included in the middleware stack
-
# only in the development environment; specifically, when +config.cache_classes+
-
# is false. Callbacks may be registered even when it is not included in the
-
# middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
-
# or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.
-
#
-
2
class Reloader
-
2
include ActiveSupport::Callbacks
-
2
include ActiveSupport::Deprecation::Reporting
-
-
2
define_callbacks :prepare
-
2
define_callbacks :cleanup
-
-
# Add a prepare callback. Prepare callbacks are run before each request, prior
-
# to ActionDispatch::Callback's before callbacks.
-
2
def self.to_prepare(*args, &block)
-
8
unless block_given?
-
warn "to_prepare without a block is deprecated. Please use a block"
-
end
-
8
set_callback(:prepare, *args, &block)
-
end
-
-
# Add a cleanup callback. Cleanup callbacks are run after each request is
-
# complete (after #close is called on the response body).
-
2
def self.to_cleanup(*args, &block)
-
unless block_given?
-
warn "to_cleanup without a block is deprecated. Please use a block"
-
end
-
set_callback(:cleanup, *args, &block)
-
end
-
-
# Execute all prepare callbacks.
-
2
def self.prepare!
-
2
new(nil).prepare!
-
end
-
-
# Execute all cleanup callbacks.
-
2
def self.cleanup!
-
new(nil).cleanup!
-
end
-
-
2
def initialize(app, condition=nil)
-
2
@app = app
-
2
@condition = condition || lambda { true }
-
2
@validated = true
-
end
-
-
2
def call(env)
-
@validated = @condition.call
-
prepare!
-
-
response = @app.call(env)
-
response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
-
-
response
-
rescue Exception
-
cleanup!
-
raise
-
end
-
-
2
def prepare! #:nodoc:
-
2
run_callbacks :prepare if validated?
-
end
-
-
2
def cleanup! #:nodoc:
-
run_callbacks :cleanup if validated?
-
ensure
-
@validated = true
-
end
-
-
2
private
-
-
2
def validated? #:nodoc:
-
2
@validated
-
end
-
end
-
end
-
2
require 'ipaddr'
-
-
2
module ActionDispatch
-
# This middleware calculates the IP address of the remote client that is
-
# making the request. It does this by checking various headers that could
-
# contain the address, and then picking the last-set address that is not
-
# on the list of trusted IPs. This follows the precedent set by e.g.
-
# {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
-
# with {reasoning explained at length}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
-
# by @gingerlime. A more detailed explanation of the algorithm is given
-
# at GetIp#calculate_ip.
-
#
-
# Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
-
# requires. Some Rack servers simply drop preceding headers, and only report
-
# the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
-
# If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
-
# then you should test your Rack server to make sure your data is good.
-
#
-
# IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
-
# This middleware assumes that there is at least one proxy sitting around
-
# and setting headers with the client's remote IP address. If you don't use
-
# a proxy, because you are hosted on e.g. Heroku without SSL, any client can
-
# claim to have any IP address by setting the X-Forwarded-For header. If you
-
# care about that, then you need to explicitly drop or ignore those headers
-
# sometime before this middleware runs.
-
2
class RemoteIp
-
2
class IpSpoofAttackError < StandardError; end
-
-
# The default trusted IPs list simply includes IP addresses that are
-
# guaranteed by the IP specification to be private addresses. Those will
-
# not be the ultimate client IP in production, and so are discarded. See
-
# http://en.wikipedia.org/wiki/Private_network for details.
-
2
TRUSTED_PROXIES = [
-
"127.0.0.1", # localhost IPv4
-
"::1", # localhost IPv6
-
"fc00::/7", # private IPv6 range fc00::/7
-
"10.0.0.0/8", # private IPv4 range 10.x.x.x
-
"172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
-
"192.168.0.0/16", # private IPv4 range 192.168.x.x
-
12
].map { |proxy| IPAddr.new(proxy) }
-
-
2
attr_reader :check_ip, :proxies
-
-
# Create a new +RemoteIp+ middleware instance.
-
#
-
# The +check_ip_spoofing+ option is on by default. When on, an exception
-
# is raised if it looks like the client is trying to lie about its own IP
-
# address. It makes sense to turn off this check on sites aimed at non-IP
-
# clients (like WAP devices), or behind proxies that set headers in an
-
# incorrect or confusing way (like AWS ELB).
-
#
-
# The +custom_proxies+ argument can take an Array of string, IPAddr, or
-
# Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
-
# single string, IPAddr, or Regexp object is provided, it will be used in
-
# addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
-
# want in the middle (or at the beginning) of the X-Forwarded-For list,
-
# with your proxy servers after it. If your proxies aren't removed, pass
-
# them in via the +custom_proxies+ parameter. That way, the middleware will
-
# ignore those IP addresses, and return the one that you want.
-
2
def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
-
2
@app = app
-
2
@check_ip = check_ip_spoofing
-
2
@proxies = if custom_proxies.blank?
-
2
TRUSTED_PROXIES
-
elsif custom_proxies.respond_to?(:any?)
-
custom_proxies
-
else
-
Array(custom_proxies) + TRUSTED_PROXIES
-
end
-
end
-
-
# Since the IP address may not be needed, we store the object here
-
# without calculating the IP to keep from slowing down the majority of
-
# requests. For those requests that do need to know the IP, the
-
# GetIp#calculate_ip method will calculate the memoized client IP address.
-
2
def call(env)
-
env["action_dispatch.remote_ip"] = GetIp.new(env, self)
-
@app.call(env)
-
end
-
-
# The GetIp class exists as a way to defer processing of the request data
-
# into an actual IP address. If the ActionDispatch::Request#remote_ip method
-
# is called, this class will calculate the value and then memoize it.
-
2
class GetIp
-
2
def initialize(env, middleware)
-
@env = env
-
@check_ip = middleware.check_ip
-
@proxies = middleware.proxies
-
end
-
-
# Sort through the various IP address headers, looking for the IP most
-
# likely to be the address of the actual remote client making this
-
# request.
-
#
-
# REMOTE_ADDR will be correct if the request is made directly against the
-
# Ruby process, on e.g. Heroku. When the request is proxied by another
-
# server like HAProxy or NGINX, the IP address that made the original
-
# request will be put in an X-Forwarded-For header. If there are multiple
-
# proxies, that header may contain a list of IPs. Other proxy services
-
# set the Client-Ip header instead, so we check that too.
-
#
-
# As discussed in {this post about Rails IP Spoofing}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
-
# while the first IP in the list is likely to be the "originating" IP,
-
# it could also have been set by the client maliciously.
-
#
-
# In order to find the first address that is (probably) accurate, we
-
# take the list of IPs, remove known and trusted proxies, and then take
-
# the last address left, which was presumably set by one of those proxies.
-
2
def calculate_ip
-
# Set by the Rack web server, this is a single value.
-
remote_addr = ips_from('REMOTE_ADDR').last
-
-
# Could be a CSV list and/or repeated headers that were concatenated.
-
client_ips = ips_from('HTTP_CLIENT_IP').reverse
-
forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse
-
-
# +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
-
# If they are both set, it means that this request passed through two
-
# proxies with incompatible IP header conventions, and there is no way
-
# for us to determine which header is the right one after the fact.
-
# Since we have no idea, we give up and explode.
-
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
-
if should_check_ip && !forwarded_ips.include?(client_ips.last)
-
# We don't know which came from the proxy, and which from the user
-
raise IpSpoofAttackError, "IP spoofing attack?! " +
-
"HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " +
-
"HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}"
-
end
-
-
# We assume these things about the IP headers:
-
#
-
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
-
# - Client-Ip is propagated from the outermost proxy, or is blank
-
# - REMOTE_ADDR will be the IP that made the request to Rack
-
ips = [forwarded_ips, client_ips, remote_addr].flatten.compact
-
-
# If every single IP option is in the trusted list, just return REMOTE_ADDR
-
filter_proxies(ips).first || remote_addr
-
end
-
-
# Memoizes the value returned by #calculate_ip and returns it for
-
# ActionDispatch::Request to use.
-
2
def to_s
-
@ip ||= calculate_ip
-
end
-
-
2
protected
-
-
2
def ips_from(header)
-
# Split the comma-separated list into an array of strings
-
ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
-
ips.select do |ip|
-
begin
-
# Only return IPs that are valid according to the IPAddr#new method
-
range = IPAddr.new(ip).to_range
-
# we want to make sure nobody is sneaking a netmask in
-
range.begin == range.end
-
rescue ArgumentError
-
nil
-
end
-
end
-
end
-
-
2
def filter_proxies(ips)
-
ips.reject do |ip|
-
@proxies.any? { |proxy| proxy === ip }
-
end
-
end
-
-
end
-
-
end
-
end
-
2
require 'securerandom'
-
2
require 'active_support/core_ext/string/access'
-
-
2
module ActionDispatch
-
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
-
# ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
-
#
-
# The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
-
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
-
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
-
#
-
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
-
# from multiple pieces of the stack.
-
2
class RequestId
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id
-
@app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = env["action_dispatch.request_id"] }
-
end
-
-
2
private
-
2
def external_request_id(env)
-
if request_id = env["HTTP_X_REQUEST_ID"].presence
-
request_id.gsub(/[^\w\-]/, "").first(255)
-
end
-
end
-
-
2
def internal_request_id
-
SecureRandom.uuid
-
end
-
end
-
end
-
2
require 'rack/utils'
-
2
require 'rack/request'
-
2
require 'rack/session/abstract/id'
-
2
require 'action_dispatch/middleware/cookies'
-
2
require 'action_dispatch/request/session'
-
-
2
module ActionDispatch
-
2
module Session
-
2
class SessionRestoreError < StandardError #:nodoc:
-
2
attr_reader :original_exception
-
-
2
def initialize(const_error)
-
@original_exception = const_error
-
-
super("Session contains objects whose class definition isn't available.\n" +
-
"Remember to require the classes for all objects kept in the session.\n" +
-
"(Original exception: #{const_error.message} [#{const_error.class}])\n")
-
end
-
end
-
-
2
module Compatibility
-
2
def initialize(app, options = {})
-
2
options[:key] ||= '_session_id'
-
2
super
-
end
-
-
2
def generate_sid
-
sid = SecureRandom.hex(16)
-
sid.encode!(Encoding::UTF_8)
-
sid
-
end
-
-
2
protected
-
-
2
def initialize_sid
-
2
@default_options.delete(:sidbits)
-
2
@default_options.delete(:secure_random)
-
end
-
end
-
-
2
module StaleSessionCheck
-
2
def load_session(env)
-
stale_session_check! { super }
-
end
-
-
2
def extract_session_id(env)
-
stale_session_check! { super }
-
end
-
-
2
def stale_session_check!
-
yield
-
rescue ArgumentError => argument_error
-
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
-
begin
-
# Note that the regexp does not allow $1 to end with a ':'
-
$1.constantize
-
rescue LoadError, NameError => e
-
raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace
-
end
-
retry
-
else
-
raise
-
end
-
end
-
end
-
-
2
module SessionObject # :nodoc:
-
2
def prepare_session(env)
-
Request::Session.create(self, env, @default_options)
-
end
-
-
2
def loaded_session?(session)
-
!session.is_a?(Request::Session) || session.loaded?
-
end
-
end
-
-
2
class AbstractStore < Rack::Session::Abstract::ID
-
2
include Compatibility
-
2
include StaleSessionCheck
-
2
include SessionObject
-
-
2
private
-
-
2
def set_cookie(env, session_id, cookie)
-
request = ActionDispatch::Request.new(env)
-
request.cookie_jar[key] = cookie
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/keys'
-
2
require 'action_dispatch/middleware/session/abstract_store'
-
2
require 'rack/session/cookie'
-
-
2
module ActionDispatch
-
2
module Session
-
# This cookie-based session store is the Rails default. It is
-
# dramatically faster than the alternatives.
-
#
-
# Sessions typically contain at most a user_id and flash message; both fit
-
# within the 4K cookie size limit. A CookieOverflow exception is raised if
-
# you attempt to store more than 4K of data.
-
#
-
# The cookie jar used for storage is automatically configured to be the
-
# best possible option given your application's configuration.
-
#
-
# If you only have secret_token set, your cookies will be signed, but
-
# not encrypted. This means a user cannot alter their +user_id+ without
-
# knowing your app's secret key, but can easily read their +user_id+. This
-
# was the default for Rails 3 apps.
-
#
-
# If you have secret_key_base set, your cookies will be encrypted. This
-
# goes a step further than signed cookies in that encrypted cookies cannot
-
# be altered or read by users. This is the default starting in Rails 4.
-
#
-
# If you have both secret_token and secret_key base set, your cookies will
-
# be encrypted, and signed cookies generated by Rails 3 will be
-
# transparently read and encrypted to provide a smooth upgrade path.
-
#
-
# Configure your session store in config/initializers/session_store.rb:
-
#
-
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
-
#
-
# Configure your secret key in config/secrets.yml:
-
#
-
# development:
-
# secret_key_base: 'secret key'
-
#
-
# To generate a secret key for an existing application, run `rake secret`.
-
#
-
# If you are upgrading an existing Rails 3 app, you should leave your
-
# existing secret_token in place and simply add the new secret_key_base.
-
# Note that you should wait to set secret_key_base until you have 100% of
-
# your userbase on Rails 4 and are reasonably sure you will not need to
-
# rollback to Rails 3. This is because cookies signed based on the new
-
# secret_key_base in Rails 4 are not backwards compatible with Rails 3.
-
# You are free to leave your existing secret_token in place, not set the
-
# new secret_key_base, and ignore the deprecation warnings until you are
-
# reasonably sure that your upgrade is otherwise complete. Additionally,
-
# you should take care to make sure you are not relying on the ability to
-
# decode signed cookies generated by your app in external applications or
-
# JavaScript before upgrading.
-
#
-
# Note that changing the secret key will invalidate all existing sessions!
-
2
class CookieStore < Rack::Session::Abstract::ID
-
2
include Compatibility
-
2
include StaleSessionCheck
-
2
include SessionObject
-
-
2
def initialize(app, options={})
-
2
super(app, options.merge!(:cookie_only => true))
-
end
-
-
2
def destroy_session(env, session_id, options)
-
new_sid = generate_sid unless options[:drop]
-
# Reset hash and Assign the new session id
-
env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
-
new_sid
-
end
-
-
2
def load_session(env)
-
stale_session_check! do
-
data = unpacked_cookie_data(env)
-
data = persistent_session_id!(data)
-
[data["session_id"], data]
-
end
-
end
-
-
2
private
-
-
2
def extract_session_id(env)
-
stale_session_check! do
-
unpacked_cookie_data(env)["session_id"]
-
end
-
end
-
-
2
def unpacked_cookie_data(env)
-
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
-
stale_session_check! do
-
if data = get_cookie(env)
-
data.stringify_keys!
-
end
-
data || {}
-
end
-
end
-
end
-
-
2
def persistent_session_id!(data, sid=nil)
-
data ||= {}
-
data["session_id"] ||= sid || generate_sid
-
data
-
end
-
-
2
def set_session(env, sid, session_data, options)
-
session_data["session_id"] = sid
-
session_data
-
end
-
-
2
def set_cookie(env, session_id, cookie)
-
cookie_jar(env)[@key] = cookie
-
end
-
-
2
def get_cookie(env)
-
cookie_jar(env)[@key]
-
end
-
-
2
def cookie_jar(env)
-
request = ActionDispatch::Request.new(env)
-
request.cookie_jar.signed_or_encrypted
-
end
-
end
-
end
-
end
-
2
require 'action_dispatch/http/request'
-
2
require 'action_dispatch/middleware/exception_wrapper'
-
-
2
module ActionDispatch
-
# This middleware rescues any exception returned by the application
-
# and calls an exceptions app that will wrap it in a format for the end user.
-
#
-
# The exceptions app should be passed as parameter on initialization
-
# of ShowExceptions. Every time there is an exception, ShowExceptions will
-
# store the exception in env["action_dispatch.exception"], rewrite the
-
# PATH_INFO to the exception status code and call the rack app.
-
#
-
# If the application returns a "X-Cascade" pass response, this middleware
-
# will send an empty response as result with the correct status code.
-
# If any exception happens inside the exceptions app, this middleware
-
# catches the exceptions and returns a FAILSAFE_RESPONSE.
-
2
class ShowExceptions
-
2
FAILSAFE_RESPONSE = [500, { 'Content-Type' => 'text/plain' },
-
["500 Internal Server Error\n" \
-
"If you are the administrator of this website, then please read this web " \
-
"application's log file and/or the web server's log file to find out what " \
-
"went wrong."]]
-
-
2
def initialize(app, exceptions_app)
-
2
@app = app
-
2
@exceptions_app = exceptions_app
-
end
-
-
2
def call(env)
-
@app.call(env)
-
rescue Exception => exception
-
if env['action_dispatch.show_exceptions'] == false
-
raise exception
-
else
-
render_exception(env, exception)
-
end
-
end
-
-
2
private
-
-
2
def render_exception(env, exception)
-
wrapper = ExceptionWrapper.new(env, exception)
-
status = wrapper.status_code
-
env["action_dispatch.exception"] = wrapper.exception
-
env["action_dispatch.original_path"] = env["PATH_INFO"]
-
env["PATH_INFO"] = "/#{status}"
-
response = @exceptions_app.call(env)
-
response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
-
rescue Exception => failsafe_error
-
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
-
FAILSAFE_RESPONSE
-
end
-
-
2
def pass_response(status)
-
[status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []]
-
end
-
end
-
end
-
2
require "active_support/inflector/methods"
-
2
require "active_support/dependencies"
-
-
2
module ActionDispatch
-
2
class MiddlewareStack
-
2
class Middleware
-
2
attr_reader :args, :block, :name, :classcache
-
-
2
def initialize(klass_or_name, *args, &block)
-
44
@klass = nil
-
-
44
if klass_or_name.respond_to?(:name)
-
40
@klass = klass_or_name
-
40
@name = @klass.name
-
else
-
4
@name = klass_or_name.to_s
-
end
-
-
44
@classcache = ActiveSupport::Dependencies::Reference
-
44
@args, @block = args, block
-
end
-
-
2
def klass
-
44
@klass || classcache[@name]
-
end
-
-
2
def ==(middleware)
-
52
case middleware
-
when Middleware
-
klass == middleware.klass
-
when Class
-
klass == middleware
-
else
-
52
normalize(@name) == normalize(middleware)
-
end
-
end
-
-
2
def inspect
-
klass.to_s
-
end
-
-
2
def build(app)
-
44
klass.new(app, *args, &block)
-
end
-
-
2
private
-
-
2
def normalize(object)
-
104
object.to_s.strip.sub(/^::/, '')
-
end
-
end
-
-
2
include Enumerable
-
-
2
attr_accessor :middlewares
-
-
2
def initialize(*args)
-
4
@middlewares = []
-
4
yield(self) if block_given?
-
end
-
-
2
def each
-
@middlewares.each { |x| yield x }
-
end
-
-
2
def size
-
middlewares.size
-
end
-
-
2
def last
-
middlewares.last
-
end
-
-
2
def [](i)
-
middlewares[i]
-
end
-
-
2
def unshift(*args, &block)
-
middleware = self.class::Middleware.new(*args, &block)
-
middlewares.unshift(middleware)
-
end
-
-
2
def initialize_copy(other)
-
12
self.middlewares = other.middlewares.dup
-
end
-
-
2
def insert(index, *args, &block)
-
6
index = assert_index(index, :before)
-
6
middleware = self.class::Middleware.new(*args, &block)
-
6
middlewares.insert(index, middleware)
-
end
-
-
2
alias_method :insert_before, :insert
-
-
2
def insert_after(index, *args, &block)
-
4
index = assert_index(index, :after)
-
4
insert(index + 1, *args, &block)
-
end
-
-
2
def swap(target, *args, &block)
-
index = assert_index(target, :before)
-
insert(index, *args, &block)
-
middlewares.delete_at(index + 1)
-
end
-
-
2
def delete(target)
-
middlewares.delete target
-
end
-
-
2
def use(*args, &block)
-
38
middleware = self.class::Middleware.new(*args, &block)
-
38
middlewares.push(middleware)
-
end
-
-
2
def build(app = nil, &block)
-
2
app ||= block
-
2
raise "MiddlewareStack#build requires an app" unless app
-
46
middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
-
end
-
-
2
protected
-
-
2
def assert_index(index, where)
-
10
i = index.is_a?(Integer) ? index : middlewares.index(index)
-
10
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
-
10
i
-
end
-
end
-
end
-
2
require 'rack/utils'
-
2
require 'active_support/core_ext/uri'
-
-
2
module ActionDispatch
-
# This middleware returns a file's contents from disk in the body response.
-
# When initialized it can accept an optional 'Cache-Control' header which
-
# will be set when a response containing a file's contents is delivered.
-
#
-
# This middleware will render the file specified in `env["PATH_INFO"]`
-
# where the base path is in the +root+ directory. For example if the +root+
-
# is set to `public/` then a request with `env["PATH_INFO"]` of
-
# `assets/application.js` will return a response with contents of a file
-
# located at `public/assets/application.js` if the file exists. If the file
-
# does not exist a 404 "File not Found" response will be returned.
-
2
class FileHandler
-
2
def initialize(root, cache_control)
-
2
@root = root.chomp('/')
-
2
@compiled_root = /^#{Regexp.escape(root)}/
-
2
headers = cache_control && { 'Cache-Control' => cache_control }
-
2
@file_server = ::Rack::File.new(@root, headers)
-
end
-
-
2
def match?(path)
-
path = URI.parser.unescape(path)
-
return false unless valid_path?(path)
-
-
paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"].map { |v|
-
Rack::Utils.clean_path_info v
-
}
-
-
if match = paths.detect { |p|
-
path = File.join(@root, p.force_encoding('UTF-8'))
-
begin
-
File.file?(path) && File.readable?(path)
-
rescue SystemCallError
-
false
-
end
-
-
}
-
return ::Rack::Utils.escape(match)
-
end
-
end
-
-
2
def call(env)
-
path = env['PATH_INFO']
-
gzip_path = gzip_file_path(path)
-
-
if gzip_path && gzip_encoding_accepted?(env)
-
env['PATH_INFO'] = gzip_path
-
status, headers, body = @file_server.call(env)
-
if status == 304
-
return [status, headers, body]
-
end
-
headers['Content-Encoding'] = 'gzip'
-
headers['Content-Type'] = content_type(path)
-
else
-
status, headers, body = @file_server.call(env)
-
end
-
-
headers['Vary'] = 'Accept-Encoding' if gzip_path
-
-
return [status, headers, body]
-
ensure
-
env['PATH_INFO'] = path
-
end
-
-
2
private
-
2
def ext
-
::ActionController::Base.default_static_extension
-
end
-
-
2
def content_type(path)
-
::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
-
end
-
-
2
def gzip_encoding_accepted?(env)
-
env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
-
end
-
-
2
def gzip_file_path(path)
-
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
-
gzip_path = "#{path}.gz"
-
if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
-
gzip_path
-
else
-
false
-
end
-
end
-
-
2
def valid_path?(path)
-
path.valid_encoding? && !path.include?("\0")
-
end
-
end
-
-
# This middleware will attempt to return the contents of a file's body from
-
# disk in the response. If a file is not found on disk, the request will be
-
# delegated to the application stack. This middleware is commonly initialized
-
# to serve assets from a server's `public/` directory.
-
#
-
# This middleware verifies the path to ensure that only files
-
# living in the root directory can be rendered. A request cannot
-
# produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
-
# requests will result in a file being returned.
-
2
class Static
-
2
def initialize(app, path, cache_control=nil)
-
2
@app = app
-
2
@file_handler = FileHandler.new(path, cache_control)
-
end
-
-
2
def call(env)
-
case env['REQUEST_METHOD']
-
when 'GET', 'HEAD'
-
path = env['PATH_INFO'].chomp('/')
-
if match = @file_handler.match?(path)
-
env["PATH_INFO"] = match
-
return @file_handler.call(env)
-
end
-
end
-
-
@app.call(env)
-
end
-
end
-
end
-
2
require 'rack/session/abstract/id'
-
-
2
module ActionDispatch
-
2
class Request < Rack::Request
-
# Session is responsible for lazily loading the session from store.
-
2
class Session # :nodoc:
-
2
ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc:
-
2
ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc:
-
-
# Singleton object used to determine if an optional param wasn't specified
-
2
Unspecified = Object.new
-
-
2
def self.create(store, env, default_options)
-
session_was = find env
-
session = Request::Session.new(store, env)
-
session.merge! session_was if session_was
-
-
set(env, session)
-
Options.set(env, Request::Session::Options.new(store, env, default_options))
-
session
-
end
-
-
2
def self.find(env)
-
env[ENV_SESSION_KEY]
-
end
-
-
2
def self.set(env, session)
-
29
env[ENV_SESSION_KEY] = session
-
end
-
-
2
class Options #:nodoc:
-
2
def self.set(env, options)
-
29
env[ENV_SESSION_OPTIONS_KEY] = options
-
end
-
-
2
def self.find(env)
-
env[ENV_SESSION_OPTIONS_KEY]
-
end
-
-
2
def initialize(by, env, default_options)
-
@by = by
-
@env = env
-
@delegate = default_options.dup
-
end
-
-
2
def [](key)
-
if key == :id
-
@delegate.fetch(key) {
-
@delegate[:id] = @by.send(:extract_session_id, @env)
-
}
-
else
-
@delegate[key]
-
end
-
end
-
-
2
def []=(k,v); @delegate[k] = v; end
-
2
def to_hash; @delegate.dup; end
-
2
def values_at(*args); @delegate.values_at(*args); end
-
end
-
-
2
def initialize(by, env)
-
@by = by
-
@env = env
-
@delegate = {}
-
@loaded = false
-
@exists = nil # we haven't checked yet
-
end
-
-
2
def id
-
options[:id]
-
end
-
-
2
def options
-
Options.find @env
-
end
-
-
2
def destroy
-
clear
-
options = self.options || {}
-
new_sid = @by.send(:destroy_session, @env, options[:id], options)
-
options[:id] = new_sid # Reset session id with a new value or nil
-
-
# Load the new sid to be written with the response
-
@loaded = false
-
load_for_write!
-
end
-
-
2
def [](key)
-
load_for_read!
-
@delegate[key.to_s]
-
end
-
-
2
def has_key?(key)
-
load_for_read!
-
@delegate.key?(key.to_s)
-
end
-
2
alias :key? :has_key?
-
2
alias :include? :has_key?
-
-
2
def keys
-
@delegate.keys
-
end
-
-
2
def values
-
@delegate.values
-
end
-
-
2
def []=(key, value)
-
load_for_write!
-
@delegate[key.to_s] = value
-
end
-
-
2
def clear
-
load_for_write!
-
@delegate.clear
-
end
-
-
2
def to_hash
-
load_for_read!
-
@delegate.dup.delete_if { |_,v| v.nil? }
-
end
-
-
2
def update(hash)
-
load_for_write!
-
@delegate.update stringify_keys(hash)
-
end
-
-
2
def delete(key)
-
load_for_write!
-
@delegate.delete key.to_s
-
end
-
-
2
def fetch(key, default=Unspecified, &block)
-
load_for_read!
-
if default == Unspecified
-
@delegate.fetch(key.to_s, &block)
-
else
-
@delegate.fetch(key.to_s, default, &block)
-
end
-
end
-
-
2
def inspect
-
if loaded?
-
super
-
else
-
"#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>"
-
end
-
end
-
-
2
def exists?
-
return @exists unless @exists.nil?
-
@exists = @by.send(:session_exists?, @env)
-
end
-
-
2
def loaded?
-
@loaded
-
end
-
-
2
def empty?
-
load_for_read!
-
@delegate.empty?
-
end
-
-
2
def merge!(other)
-
load_for_write!
-
@delegate.merge!(other)
-
end
-
-
2
private
-
-
2
def load_for_read!
-
load! if !loaded? && exists?
-
end
-
-
2
def load_for_write!
-
load! unless loaded?
-
end
-
-
2
def load!
-
id, session = @by.load_session @env
-
options[:id] = id
-
@delegate.replace(stringify_keys(session))
-
@loaded = true
-
end
-
-
2
def stringify_keys(other)
-
other.each_with_object({}) { |(key, value), hash|
-
hash[key.to_s] = value
-
}
-
end
-
end
-
end
-
end
-
2
module ActionDispatch
-
2
class Request < Rack::Request
-
2
class Utils # :nodoc:
-
-
2
mattr_accessor :perform_deep_munge
-
2
self.perform_deep_munge = true
-
-
2
class << self
-
# Remove nils from the params hash
-
2
def deep_munge(hash, keys = [])
-
29
return hash unless perform_deep_munge
-
-
29
hash.each do |k, v|
-
keys << k
-
case v
-
when Array
-
v.grep(Hash) { |x| deep_munge(x, keys) }
-
v.compact!
-
if v.empty?
-
hash[k] = nil
-
ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys)
-
end
-
when Hash
-
deep_munge(v, keys)
-
end
-
keys.pop
-
end
-
-
29
hash
-
end
-
end
-
end
-
end
-
end
-
-
2
require 'delegate'
-
2
require 'active_support/core_ext/string/strip'
-
-
2
module ActionDispatch
-
2
module Routing
-
2
class RouteWrapper < SimpleDelegator
-
2
def endpoint
-
app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
-
end
-
-
2
def constraints
-
requirements.except(:controller, :action)
-
end
-
-
2
def rack_app
-
app.app
-
end
-
-
2
def verb
-
super.source.gsub(/[$^]/, '')
-
end
-
-
2
def path
-
super.spec.to_s
-
end
-
-
2
def name
-
super.to_s
-
end
-
-
2
def regexp
-
__getobj__.path.to_regexp
-
end
-
-
2
def json_regexp
-
str = regexp.inspect.
-
sub('\\A' , '^').
-
sub('\\Z' , '$').
-
sub('\\z' , '$').
-
sub(/^\// , '').
-
sub(/\/[a-z]*$/ , '').
-
gsub(/\(\?#.+\)/ , '').
-
gsub(/\(\?-\w+:/ , '(').
-
gsub(/\s/ , '')
-
Regexp.new(str).source
-
end
-
-
2
def reqs
-
@reqs ||= begin
-
reqs = endpoint
-
reqs += " #{constraints}" unless constraints.empty?
-
reqs
-
end
-
end
-
-
2
def controller
-
requirements[:controller] || ':controller'
-
end
-
-
2
def action
-
requirements[:action] || ':action'
-
end
-
-
2
def internal?
-
controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z}
-
end
-
-
2
def engine?
-
rack_app.respond_to?(:routes)
-
end
-
end
-
-
##
-
# This class is just used for displaying route information when someone
-
# executes `rake routes` or looks at the RoutingError page.
-
# People should not use this class.
-
2
class RoutesInspector # :nodoc:
-
2
def initialize(routes)
-
@engines = {}
-
@routes = routes
-
end
-
-
2
def format(formatter, filter = nil)
-
routes_to_display = filter_routes(filter)
-
-
routes = collect_routes(routes_to_display)
-
-
if routes.none?
-
formatter.no_routes
-
return formatter.result
-
end
-
-
formatter.header routes
-
formatter.section routes
-
-
@engines.each do |name, engine_routes|
-
formatter.section_title "Routes for #{name}"
-
formatter.section engine_routes
-
end
-
-
formatter.result
-
end
-
-
2
private
-
-
2
def filter_routes(filter)
-
if filter
-
@routes.select { |route| route.defaults[:controller] == filter }
-
else
-
@routes
-
end
-
end
-
-
2
def collect_routes(routes)
-
routes.collect do |route|
-
RouteWrapper.new(route)
-
end.reject do |route|
-
route.internal?
-
end.collect do |route|
-
collect_engine_routes(route)
-
-
{ name: route.name,
-
verb: route.verb,
-
path: route.path,
-
reqs: route.reqs,
-
regexp: route.json_regexp }
-
end
-
end
-
-
2
def collect_engine_routes(route)
-
name = route.endpoint
-
return unless route.engine?
-
return if @engines[name]
-
-
routes = route.rack_app.routes
-
if routes.is_a?(ActionDispatch::Routing::RouteSet)
-
@engines[name] = collect_routes(routes.routes)
-
end
-
end
-
end
-
-
2
class ConsoleFormatter
-
2
def initialize
-
@buffer = []
-
end
-
-
2
def result
-
@buffer.join("\n")
-
end
-
-
2
def section_title(title)
-
@buffer << "\n#{title}:"
-
end
-
-
2
def section(routes)
-
@buffer << draw_section(routes)
-
end
-
-
2
def header(routes)
-
@buffer << draw_header(routes)
-
end
-
-
2
def no_routes
-
@buffer << <<-MESSAGE.strip_heredoc
-
You don't have any routes defined!
-
-
Please add some routes in config/routes.rb.
-
-
For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
-
MESSAGE
-
end
-
-
2
private
-
2
def draw_section(routes)
-
header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length)
-
name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
-
-
routes.map do |r|
-
"#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
-
end
-
end
-
-
2
def draw_header(routes)
-
name_width, verb_width, path_width = widths(routes)
-
-
"#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
-
end
-
-
2
def widths(routes)
-
[routes.map { |r| r[:name].length }.max || 0,
-
routes.map { |r| r[:verb].length }.max || 0,
-
routes.map { |r| r[:path].length }.max || 0]
-
end
-
end
-
-
2
class HtmlTableFormatter
-
2
def initialize(view)
-
@view = view
-
@buffer = []
-
end
-
-
2
def section_title(title)
-
@buffer << %(<tr><th colspan="4">#{title}</th></tr>)
-
end
-
-
2
def section(routes)
-
@buffer << @view.render(partial: "routes/route", collection: routes)
-
end
-
-
# the header is part of the HTML page, so we don't construct it here.
-
2
def header(routes)
-
end
-
-
2
def no_routes
-
@buffer << <<-MESSAGE.strip_heredoc
-
<p>You don't have any routes defined!</p>
-
<ul>
-
<li>Please add some routes in <tt>config/routes.rb</tt>.</li>
-
<li>
-
For more information about routes, please see the Rails guide
-
<a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
-
</li>
-
</ul>
-
MESSAGE
-
end
-
-
2
def result
-
@view.raw @view.render(layout: "routes/table") {
-
@view.raw @buffer.join("\n")
-
}
-
end
-
end
-
end
-
end
-
2
require 'rails-dom-testing'
-
-
2
module ActionDispatch
-
2
module Assertions
-
2
autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
-
2
autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
-
-
2
extend ActiveSupport::Concern
-
-
2
include ResponseAssertions
-
2
include RoutingAssertions
-
2
include Rails::Dom::Testing::Assertions
-
-
2
def html_document
-
@html_document ||= if @response.content_type.to_s =~ /xml$/
-
Nokogiri::XML::Document.parse(@response.body)
-
else
-
Nokogiri::HTML::Document.parse(@response.body)
-
end
-
end
-
end
-
end
-
-
2
module ActionDispatch
-
2
module Assertions
-
# A small suite of assertions that test responses from \Rails applications.
-
2
module ResponseAssertions
-
# Asserts that the response is one of the following types:
-
#
-
# * <tt>:success</tt> - Status code was in the 200-299 range
-
# * <tt>:redirect</tt> - Status code was in the 300-399 range
-
# * <tt>:missing</tt> - Status code was 404
-
# * <tt>:error</tt> - Status code was in the 500-599 range
-
#
-
# You can also pass an explicit status number like <tt>assert_response(501)</tt>
-
# or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
-
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
-
#
-
# # assert that the response was a redirection
-
# assert_response :redirect
-
#
-
# # assert that the response code was status code 401 (unauthorized)
-
# assert_response 401
-
2
def assert_response(type, message = nil)
-
22
message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>"
-
-
22
if Symbol === type
-
22
if [:success, :missing, :redirect, :error].include?(type)
-
22
assert @response.send("#{type}?"), message
-
else
-
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
-
if code.nil?
-
raise ArgumentError, "Invalid response type :#{type}"
-
end
-
assert_equal code, @response.response_code, message
-
end
-
else
-
assert_equal type, @response.response_code, message
-
end
-
end
-
-
# Assert that the redirection options passed in match those of the redirect called in the latest action.
-
# This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also
-
# match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on.
-
#
-
# # assert that the redirection was to the "index" action on the WeblogController
-
# assert_redirected_to controller: "weblog", action: "index"
-
#
-
# # assert that the redirection was to the named route login_url
-
# assert_redirected_to login_url
-
#
-
# # assert that the redirection was to the url for @customer
-
# assert_redirected_to @customer
-
#
-
# # asserts that the redirection matches the regular expression
-
# assert_redirected_to %r(\Ahttp://example.org)
-
2
def assert_redirected_to(options = {}, message=nil)
-
10
assert_response(:redirect, message)
-
9
return true if options === @response.location
-
-
9
redirect_is = normalize_argument_to_redirection(@response.location)
-
9
redirect_expected = normalize_argument_to_redirection(options)
-
-
9
message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
-
9
assert_operator redirect_expected, :===, redirect_is, message
-
end
-
-
2
private
-
# Proxy to to_param if the object will respond to it.
-
2
def parameterize(value)
-
value.respond_to?(:to_param) ? value.to_param : value
-
end
-
-
2
def normalize_argument_to_redirection(fragment)
-
18
if Regexp === fragment
-
fragment
-
else
-
18
handle = @controller || ActionController::Redirecting
-
18
handle._compute_redirect_to_location(@request, fragment)
-
end
-
end
-
end
-
end
-
end
-
2
require 'uri'
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
2
require 'active_support/core_ext/string/access'
-
2
require 'action_controller/metal/exceptions'
-
-
2
module ActionDispatch
-
2
module Assertions
-
# Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
-
2
module RoutingAssertions
-
# Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
-
# match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
-
#
-
# Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
-
# requiring a specific HTTP method. The hash should contain a :path with the incoming request path
-
# and a :method containing the required HTTP verb.
-
#
-
# # assert that POSTing to /items will call the create action on ItemsController
-
# assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
-
#
-
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
-
# to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the
-
# extras argument, appending the query string on the path directly will not work. For example:
-
#
-
# # assert that a path of '/items/list/1?view=print' returns the correct options
-
# assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
-
#
-
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
-
#
-
# # Check the default route (i.e., the index action)
-
# assert_recognizes({controller: 'items', action: 'index'}, 'items')
-
#
-
# # Test a specific action
-
# assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
-
#
-
# # Test an action with a parameter
-
# assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
-
#
-
# # Test a custom route
-
# assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
-
2
def assert_recognizes(expected_options, path, extras={}, msg=nil)
-
if path.is_a?(Hash) && path[:method].to_s == "all"
-
[:get, :post, :put, :delete].each do |method|
-
assert_recognizes(expected_options, path.merge(method: method), extras, msg)
-
end
-
else
-
request = recognized_request_for(path, extras, msg)
-
-
expected_options = expected_options.clone
-
-
expected_options.stringify_keys!
-
-
msg = message(msg, "") {
-
sprintf("The recognized options <%s> did not match <%s>, difference:",
-
request.path_parameters, expected_options)
-
}
-
-
assert_equal(expected_options, request.path_parameters, msg)
-
end
-
end
-
-
# Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
-
# The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
-
# a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
-
#
-
# The +defaults+ parameter is unused.
-
#
-
# # Asserts that the default action is generated for a route with no action
-
# assert_generates "/items", controller: "items", action: "index"
-
#
-
# # Tests that the list action is properly routed
-
# assert_generates "/items/list", controller: "items", action: "list"
-
#
-
# # Tests the generation of a route with a parameter
-
# assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
-
#
-
# # Asserts that the generated route gives us our custom route
-
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
-
2
def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
-
if expected_path =~ %r{://}
-
fail_on(URI::InvalidURIError, message) do
-
uri = URI.parse(expected_path)
-
expected_path = uri.path.to_s.empty? ? "/" : uri.path
-
end
-
else
-
expected_path = "/#{expected_path}" unless expected_path.first == '/'
-
end
-
# Load routes.rb if it hasn't been loaded.
-
-
generated_path, extra_keys = @routes.generate_extras(options, defaults)
-
found_extras = options.reject { |k, _| ! extra_keys.include? k }
-
-
msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
-
assert_equal(extras, found_extras, msg)
-
-
msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path,
-
expected_path)
-
assert_equal(expected_path, generated_path, msg)
-
end
-
-
# Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
-
# <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
-
# and +assert_generates+ into one step.
-
#
-
# The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
-
# +message+ parameter allows you to specify a custom error message to display upon failure.
-
#
-
# # Assert a basic route: a controller with the default action (index)
-
# assert_routing '/home', controller: 'home', action: 'index'
-
#
-
# # Test a route generated with a specific controller, action, and parameter (id)
-
# assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
-
#
-
# # Assert a basic route (controller + default action), with an error message if it fails
-
# assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
-
#
-
# # Tests a route, providing a defaults hash
-
# assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
-
#
-
# # Tests a route with a HTTP method
-
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
-
2
def assert_routing(path, options, defaults={}, extras={}, message=nil)
-
assert_recognizes(options, path, extras, message)
-
-
controller, default_controller = options[:controller], defaults[:controller]
-
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
-
options[:controller] = "/#{controller}"
-
end
-
-
generate_options = options.dup.delete_if{ |k, _| defaults.key?(k) }
-
assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
-
end
-
-
# A helper to make it easier to test different route configurations.
-
# This method temporarily replaces @routes
-
# with a new RouteSet instance.
-
#
-
# The new instance is yielded to the passed block. Typically the block
-
# will create some routes using <tt>set.draw { match ... }</tt>:
-
#
-
# with_routing do |set|
-
# set.draw do
-
# resources :users
-
# end
-
# assert_equal "/users", users_path
-
# end
-
#
-
2
def with_routing
-
old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
-
if defined?(@controller) && @controller
-
old_controller, @controller = @controller, @controller.clone
-
_routes = @routes
-
-
@controller.singleton_class.send(:include, _routes.url_helpers)
-
@controller.view_context_class = Class.new(@controller.view_context_class) do
-
include _routes.url_helpers
-
end
-
end
-
yield @routes
-
ensure
-
@routes = old_routes
-
if defined?(@controller) && @controller
-
@controller = old_controller
-
end
-
end
-
-
# ROUTES TODO: These assertions should really work in an integration context
-
2
def method_missing(selector, *args, &block)
-
10
if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
-
10
@controller.send(selector, *args, &block)
-
else
-
super
-
end
-
end
-
-
2
private
-
# Recognizes the route for a given path.
-
2
def recognized_request_for(path, extras = {}, msg)
-
if path.is_a?(Hash)
-
method = path[:method]
-
path = path[:path]
-
else
-
method = :get
-
end
-
-
# Assume given controller
-
request = ActionController::TestRequest.new
-
-
if path =~ %r{://}
-
fail_on(URI::InvalidURIError, msg) do
-
uri = URI.parse(path)
-
request.env["rack.url_scheme"] = uri.scheme || "http"
-
request.host = uri.host if uri.host
-
request.port = uri.port if uri.port
-
request.path = uri.path.to_s.empty? ? "/" : uri.path
-
end
-
else
-
path = "/#{path}" unless path.first == "/"
-
request.path = path
-
end
-
-
request.request_method = method if method
-
-
params = fail_on(ActionController::RoutingError, msg) do
-
@routes.recognize_path(path, { :method => method, :extras => extras })
-
end
-
request.path_parameters = params.with_indifferent_access
-
-
request
-
end
-
-
2
def fail_on(exception_class, message)
-
yield
-
rescue exception_class => e
-
raise Minitest::Assertion, message || e.message
-
end
-
end
-
end
-
end
-
2
require 'stringio'
-
2
require 'uri'
-
2
require 'active_support/core_ext/kernel/singleton_class'
-
2
require 'active_support/core_ext/object/try'
-
2
require 'rack/test'
-
2
require 'minitest'
-
-
2
module ActionDispatch
-
2
module Integration #:nodoc:
-
2
module RequestHelpers
-
# Performs a GET request with the given parameters.
-
#
-
# - +path+: The URI (as a String) on which you want to perform a GET
-
# request.
-
# - +parameters+: The HTTP parameters that you want to pass. This may
-
# be +nil+,
-
# a Hash, or a String that is appropriately encoded
-
# (<tt>application/x-www-form-urlencoded</tt> or
-
# <tt>multipart/form-data</tt>).
-
# - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be
-
# merged into the Rack env hash.
-
#
-
# This method returns a Response object, which one can use to
-
# inspect the details of the response. Furthermore, if this method was
-
# called from an ActionDispatch::IntegrationTest object, then that
-
# object's <tt>@response</tt> instance variable will point to the same
-
# response object.
-
#
-
# You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with
-
# +#post+, +#patch+, +#put+, +#delete+, and +#head+.
-
2
def get(path, parameters = nil, headers_or_env = nil)
-
process :get, path, parameters, headers_or_env
-
end
-
-
# Performs a POST request with the given parameters. See +#get+ for more
-
# details.
-
2
def post(path, parameters = nil, headers_or_env = nil)
-
process :post, path, parameters, headers_or_env
-
end
-
-
# Performs a PATCH request with the given parameters. See +#get+ for more
-
# details.
-
2
def patch(path, parameters = nil, headers_or_env = nil)
-
process :patch, path, parameters, headers_or_env
-
end
-
-
# Performs a PUT request with the given parameters. See +#get+ for more
-
# details.
-
2
def put(path, parameters = nil, headers_or_env = nil)
-
process :put, path, parameters, headers_or_env
-
end
-
-
# Performs a DELETE request with the given parameters. See +#get+ for
-
# more details.
-
2
def delete(path, parameters = nil, headers_or_env = nil)
-
process :delete, path, parameters, headers_or_env
-
end
-
-
# Performs a HEAD request with the given parameters. See +#get+ for more
-
# details.
-
2
def head(path, parameters = nil, headers_or_env = nil)
-
process :head, path, parameters, headers_or_env
-
end
-
-
# Performs an XMLHttpRequest request with the given parameters, mirroring
-
# a request from the Prototype library.
-
#
-
# The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or
-
# +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart
-
# string; the headers are a hash.
-
2
def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil)
-
headers_or_env ||= {}
-
headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
-
headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
-
process(request_method, path, parameters, headers_or_env)
-
end
-
2
alias xhr :xml_http_request
-
-
# Follow a single redirect response. If the last response was not a
-
# redirect, an exception will be raised. Otherwise, the redirect is
-
# performed on the location header.
-
2
def follow_redirect!
-
raise "not a redirect! #{status} #{status_message}" unless redirect?
-
get(response.location)
-
status
-
end
-
-
# Performs a request using the specified method, following any subsequent
-
# redirect. Note that the redirects are followed until the response is
-
# not a redirect--this means you may run into an infinite loop if your
-
# redirect loops back to itself.
-
2
def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil)
-
process(http_method, path, parameters, headers_or_env)
-
follow_redirect! while redirect?
-
status
-
end
-
-
# Performs a GET request, following any subsequent redirect.
-
# See +request_via_redirect+ for more information.
-
2
def get_via_redirect(path, parameters = nil, headers_or_env = nil)
-
request_via_redirect(:get, path, parameters, headers_or_env)
-
end
-
-
# Performs a POST request, following any subsequent redirect.
-
# See +request_via_redirect+ for more information.
-
2
def post_via_redirect(path, parameters = nil, headers_or_env = nil)
-
request_via_redirect(:post, path, parameters, headers_or_env)
-
end
-
-
# Performs a PATCH request, following any subsequent redirect.
-
# See +request_via_redirect+ for more information.
-
2
def patch_via_redirect(path, parameters = nil, headers_or_env = nil)
-
request_via_redirect(:patch, path, parameters, headers_or_env)
-
end
-
-
# Performs a PUT request, following any subsequent redirect.
-
# See +request_via_redirect+ for more information.
-
2
def put_via_redirect(path, parameters = nil, headers_or_env = nil)
-
request_via_redirect(:put, path, parameters, headers_or_env)
-
end
-
-
# Performs a DELETE request, following any subsequent redirect.
-
# See +request_via_redirect+ for more information.
-
2
def delete_via_redirect(path, parameters = nil, headers_or_env = nil)
-
request_via_redirect(:delete, path, parameters, headers_or_env)
-
end
-
end
-
-
# An instance of this class represents a set of requests and responses
-
# performed sequentially by a test process. Because you can instantiate
-
# multiple sessions and run them side-by-side, you can also mimic (to some
-
# limited extent) multiple simultaneous users interacting with your system.
-
#
-
# Typically, you will instantiate a new session using
-
# IntegrationTest#open_session, rather than instantiating
-
# Integration::Session directly.
-
2
class Session
-
2
DEFAULT_HOST = "www.example.com"
-
-
2
include Minitest::Assertions
-
2
include TestProcess, RequestHelpers, Assertions
-
-
2
%w( status status_message headers body redirect? ).each do |method|
-
10
delegate method, :to => :response, :allow_nil => true
-
end
-
-
2
%w( path ).each do |method|
-
2
delegate method, :to => :request, :allow_nil => true
-
end
-
-
# The hostname used in the last request.
-
2
def host
-
@host || DEFAULT_HOST
-
end
-
2
attr_writer :host
-
-
# The remote_addr used in the last request.
-
2
attr_accessor :remote_addr
-
-
# The Accept header to send.
-
2
attr_accessor :accept
-
-
# A map of the cookies returned by the last response, and which will be
-
# sent with the next request.
-
2
def cookies
-
_mock_session.cookie_jar
-
end
-
-
# A reference to the controller instance used by the last request.
-
2
attr_reader :controller
-
-
# A reference to the request instance used by the last request.
-
2
attr_reader :request
-
-
# A reference to the response instance used by the last request.
-
2
attr_reader :response
-
-
# A running counter of the number of requests processed.
-
2
attr_accessor :request_count
-
-
2
include ActionDispatch::Routing::UrlFor
-
-
# Create and initialize a new Session instance.
-
2
def initialize(app)
-
super()
-
@app = app
-
-
# If the app is a Rails app, make url_helpers available on the session
-
# This makes app.url_for and app.foo_path available in the console
-
if app.respond_to?(:routes)
-
singleton_class.class_eval do
-
include app.routes.url_helpers
-
include app.routes.mounted_helpers
-
end
-
end
-
-
reset!
-
end
-
-
2
def url_options
-
@url_options ||= default_url_options.dup.tap do |url_options|
-
url_options.reverse_merge!(controller.url_options) if controller
-
-
if @app.respond_to?(:routes)
-
url_options.reverse_merge!(@app.routes.default_url_options)
-
end
-
-
url_options.reverse_merge!(:host => host, :protocol => https? ? "https" : "http")
-
end
-
end
-
-
# Resets the instance. This can be used to reset the state information
-
# in an existing session instance, so it can be used from a clean-slate
-
# condition.
-
#
-
# session.reset!
-
2
def reset!
-
@https = false
-
@controller = @request = @response = nil
-
@_mock_session = nil
-
@request_count = 0
-
@url_options = nil
-
-
self.host = DEFAULT_HOST
-
self.remote_addr = "127.0.0.1"
-
self.accept = "text/xml,application/xml,application/xhtml+xml," +
-
"text/html;q=0.9,text/plain;q=0.8,image/png," +
-
"*/*;q=0.5"
-
-
unless defined? @named_routes_configured
-
# the helpers are made protected by default--we make them public for
-
# easier access during testing and troubleshooting.
-
@named_routes_configured = true
-
end
-
end
-
-
# Specify whether or not the session should mimic a secure HTTPS request.
-
#
-
# session.https!
-
# session.https!(false)
-
2
def https!(flag = true)
-
@https = flag
-
end
-
-
# Returns +true+ if the session is mimicking a secure HTTPS request.
-
#
-
# if session.https?
-
# ...
-
# end
-
2
def https?
-
@https
-
end
-
-
# Set the host name to use in the next request.
-
#
-
# session.host! "www.example.com"
-
2
alias :host! :host=
-
-
2
private
-
2
def _mock_session
-
@_mock_session ||= Rack::MockSession.new(@app, host)
-
end
-
-
# Performs the actual request.
-
2
def process(method, path, parameters = nil, headers_or_env = nil)
-
if path =~ %r{://}
-
location = URI.parse(path)
-
https! URI::HTTPS === location if location.scheme
-
host! "#{location.host}:#{location.port}" if location.host
-
path = location.query ? "#{location.path}?#{location.query}" : location.path
-
end
-
-
hostname, port = host.split(':')
-
-
env = {
-
:method => method,
-
:params => parameters,
-
-
"SERVER_NAME" => hostname,
-
"SERVER_PORT" => port || (https? ? "443" : "80"),
-
"HTTPS" => https? ? "on" : "off",
-
"rack.url_scheme" => https? ? "https" : "http",
-
-
"REQUEST_URI" => path,
-
"HTTP_HOST" => host,
-
"REMOTE_ADDR" => remote_addr,
-
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
-
"HTTP_ACCEPT" => accept
-
}
-
# this modifies the passed env directly
-
Http::Headers.new(env).merge!(headers_or_env || {})
-
-
session = Rack::Test::Session.new(_mock_session)
-
-
# NOTE: rack-test v0.5 doesn't build a default uri correctly
-
# Make sure requested path is always a full uri
-
session.request(build_full_uri(path, env), env)
-
-
@request_count += 1
-
@request = ActionDispatch::Request.new(session.last_request.env)
-
response = _mock_session.last_response
-
@response = ActionDispatch::TestResponse.from_response(response)
-
@html_document = nil
-
@html_scanner_document = nil
-
@url_options = nil
-
-
@controller = session.last_request.env['action_controller.instance']
-
-
return response.status
-
end
-
-
2
def build_full_uri(path, env)
-
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
-
end
-
end
-
-
2
module Runner
-
2
include ActionDispatch::Assertions
-
-
2
def app
-
@app ||= nil
-
end
-
-
# Reset the current session. This is useful for testing multiple sessions
-
# in a single test case.
-
2
def reset!
-
@integration_session = Integration::Session.new(app)
-
end
-
-
2
def remove! # :nodoc:
-
@integration_session = nil
-
end
-
-
%w(get post patch put head delete cookies assigns
-
2
xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
-
24
define_method(method) do |*args|
-
reset! unless integration_session
-
-
# reset the html_document variable, except for cookies/assigns calls
-
unless method == 'cookies' || method == 'assigns'
-
@html_document = nil
-
@html_scanner_document = nil
-
reset_template_assertion
-
end
-
-
integration_session.__send__(method, *args).tap do
-
copy_session_variables!
-
end
-
end
-
end
-
-
# Open a new session instance. If a block is given, the new session is
-
# yielded to the block before being returned.
-
#
-
# session = open_session do |sess|
-
# sess.extend(CustomAssertions)
-
# end
-
#
-
# By default, a single session is automatically created for you, but you
-
# can use this method to open multiple sessions that ought to be tested
-
# simultaneously.
-
2
def open_session
-
dup.tap do |session|
-
yield session if block_given?
-
end
-
end
-
-
# Copy the instance variables from the current session instance into the
-
# test instance.
-
2
def copy_session_variables! #:nodoc:
-
return unless integration_session
-
%w(controller response request).each do |var|
-
instance_variable_set("@#{var}", @integration_session.__send__(var))
-
end
-
end
-
-
2
def default_url_options
-
reset! unless integration_session
-
integration_session.default_url_options
-
end
-
-
2
def default_url_options=(options)
-
reset! unless integration_session
-
integration_session.default_url_options = options
-
end
-
-
2
def respond_to?(method, include_private = false)
-
integration_session.respond_to?(method, include_private) || super
-
end
-
-
# Delegate unhandled messages to the current session instance.
-
2
def method_missing(sym, *args, &block)
-
reset! unless integration_session
-
if integration_session.respond_to?(sym)
-
integration_session.__send__(sym, *args, &block).tap do
-
copy_session_variables!
-
end
-
else
-
super
-
end
-
end
-
-
2
private
-
2
def integration_session
-
@integration_session ||= nil
-
end
-
end
-
end
-
-
# An integration test spans multiple controllers and actions,
-
# tying them all together to ensure they work together as expected. It tests
-
# more completely than either unit or functional tests do, exercising the
-
# entire stack, from the dispatcher to the database.
-
#
-
# At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
-
# using the get/post methods:
-
#
-
# require "test_helper"
-
#
-
# class ExampleTest < ActionDispatch::IntegrationTest
-
# fixtures :people
-
#
-
# def test_login
-
# # get the login page
-
# get "/login"
-
# assert_equal 200, status
-
#
-
# # post the login and follow through to the home page
-
# post "/login", username: people(:jamis).username,
-
# password: people(:jamis).password
-
# follow_redirect!
-
# assert_equal 200, status
-
# assert_equal "/home", path
-
# end
-
# end
-
#
-
# However, you can also have multiple session instances open per test, and
-
# even extend those instances with assertions and methods to create a very
-
# powerful testing DSL that is specific for your application. You can even
-
# reference any named routes you happen to have defined.
-
#
-
# require "test_helper"
-
#
-
# class AdvancedTest < ActionDispatch::IntegrationTest
-
# fixtures :people, :rooms
-
#
-
# def test_login_and_speak
-
# jamis, david = login(:jamis), login(:david)
-
# room = rooms(:office)
-
#
-
# jamis.enter(room)
-
# jamis.speak(room, "anybody home?")
-
#
-
# david.enter(room)
-
# david.speak(room, "hello!")
-
# end
-
#
-
# private
-
#
-
# module CustomAssertions
-
# def enter(room)
-
# # reference a named route, for maximum internal consistency!
-
# get(room_url(id: room.id))
-
# assert(...)
-
# ...
-
# end
-
#
-
# def speak(room, message)
-
# xml_http_request "/say/#{room.id}", message: message
-
# assert(...)
-
# ...
-
# end
-
# end
-
#
-
# def login(who)
-
# open_session do |sess|
-
# sess.extend(CustomAssertions)
-
# who = people(who)
-
# sess.post "/login", username: who.username,
-
# password: who.password
-
# assert(...)
-
# end
-
# end
-
# end
-
2
class IntegrationTest < ActiveSupport::TestCase
-
2
include Integration::Runner
-
2
include ActionController::TemplateAssertions
-
2
include ActionDispatch::Routing::UrlFor
-
-
2
@@app = nil
-
-
2
def self.app
-
@@app || ActionDispatch.test_app
-
end
-
-
2
def self.app=(app)
-
@@app = app
-
end
-
-
2
def app
-
super || self.class.app
-
end
-
-
2
def url_options
-
reset! unless integration_session
-
integration_session.url_options
-
end
-
-
2
def document_root_element
-
html_document.root
-
end
-
end
-
end
-
2
require 'action_dispatch/middleware/cookies'
-
2
require 'action_dispatch/middleware/flash'
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
-
2
module ActionDispatch
-
2
module TestProcess
-
2
def assigns(key = nil)
-
5
assigns = {}.with_indifferent_access
-
23
@controller.view_assigns.each { |k, v| assigns.regular_writer(k, v) }
-
5
key.nil? ? assigns : assigns[key]
-
end
-
-
2
def session
-
27
@request.session
-
end
-
-
2
def flash
-
@request.flash
-
end
-
-
2
def cookies
-
@request.cookie_jar
-
end
-
-
2
def redirect_to_url
-
@response.redirect_url
-
end
-
-
# Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionController::TestCase.fixture_path, path), type)</tt>:
-
#
-
# post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
-
#
-
# To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
-
# This will not affect other platforms:
-
#
-
# post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
-
2
def fixture_file_upload(path, mime_type = nil, binary = false)
-
if self.class.respond_to?(:fixture_path) && self.class.fixture_path
-
path = File.join(self.class.fixture_path, path)
-
end
-
Rack::Test::UploadedFile.new(path, mime_type, binary)
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
2
require 'rack/utils'
-
-
2
module ActionDispatch
-
2
class TestRequest < Request
-
2
DEFAULT_ENV = Rack::MockRequest.env_for('/',
-
'HTTP_HOST' => 'test.host',
-
'REMOTE_ADDR' => '0.0.0.0',
-
'HTTP_USER_AGENT' => 'Rails Testing'
-
)
-
-
2
def self.new(env = {})
-
29
super
-
end
-
-
2
def initialize(env = {})
-
29
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
-
29
super(default_env.merge(env))
-
end
-
-
2
def request_method=(method)
-
@env['REQUEST_METHOD'] = method.to_s.upcase
-
end
-
-
2
def host=(host)
-
@env['HTTP_HOST'] = host
-
end
-
-
2
def port=(number)
-
@env['SERVER_PORT'] = number.to_i
-
end
-
-
2
def request_uri=(uri)
-
@env['REQUEST_URI'] = uri
-
end
-
-
2
def path=(path)
-
@env['PATH_INFO'] = path
-
end
-
-
2
def action=(action_name)
-
path_parameters[:action] = action_name.to_s
-
end
-
-
2
def if_modified_since=(last_modified)
-
@env['HTTP_IF_MODIFIED_SINCE'] = last_modified
-
end
-
-
2
def if_none_match=(etag)
-
@env['HTTP_IF_NONE_MATCH'] = etag
-
end
-
-
2
def remote_addr=(addr)
-
@env['REMOTE_ADDR'] = addr
-
end
-
-
2
def user_agent=(user_agent)
-
@env['HTTP_USER_AGENT'] = user_agent
-
end
-
-
2
def accept=(mime_types)
-
@env.delete('action_dispatch.request.accepts')
-
@env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",")
-
end
-
-
2
alias :rack_cookies :cookies
-
-
2
def cookies
-
56
@cookies ||= {}.with_indifferent_access
-
end
-
-
2
private
-
-
2
def default_env
-
DEFAULT_ENV
-
end
-
end
-
end
-
2
module ActionDispatch
-
# Integration test methods such as ActionDispatch::Integration::Session#get
-
# and ActionDispatch::Integration::Session#post return objects of class
-
# TestResponse, which represent the HTTP response results of the requested
-
# controller actions.
-
#
-
# See Response for more information on controller response objects.
-
2
class TestResponse < Response
-
2
def self.from_response(response)
-
new response.status, response.headers, response.body, default_headers: nil
-
end
-
-
# Was the response successful?
-
2
alias_method :success?, :successful?
-
-
# Was the URL not found?
-
2
alias_method :missing?, :not_found?
-
-
# Were we redirected?
-
2
alias_method :redirect?, :redirection?
-
-
# Was there a server-side error?
-
2
alias_method :error?, :server_error?
-
end
-
end
-
1
require 'active_support/core_ext/string/output_safety'
-
-
1
module ActionView
-
1
class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc:
-
1
def initialize(*)
-
52
super
-
52
encode!
-
end
-
-
1
def <<(value)
-
389
return self if value.nil?
-
339
super(value.to_s)
-
end
-
1
alias :append= :<<
-
-
1
def safe_expr_append=(val)
-
return self if val.nil?
-
safe_concat val.to_s
-
end
-
-
1
alias :safe_append= :safe_concat
-
end
-
-
1
class StreamingBuffer #:nodoc:
-
1
def initialize(block)
-
@block = block
-
end
-
-
1
def <<(value)
-
value = value.to_s
-
value = ERB::Util.h(value) unless value.html_safe?
-
@block.call(value)
-
end
-
1
alias :concat :<<
-
1
alias :append= :<<
-
-
1
def safe_concat(value)
-
@block.call(value.to_s)
-
end
-
1
alias :safe_append= :safe_concat
-
-
1
def html_safe?
-
true
-
end
-
-
1
def html_safe
-
self
-
end
-
end
-
end
-
2
require 'thread_safe'
-
-
2
module ActionView
-
2
class DependencyTracker # :nodoc:
-
2
@trackers = ThreadSafe::Cache.new
-
-
2
def self.find_dependencies(name, template)
-
tracker = @trackers[template.handler]
-
-
if tracker.present?
-
tracker.call(name, template)
-
else
-
[]
-
end
-
end
-
-
2
def self.register_tracker(extension, tracker)
-
4
handler = Template.handler_for_extension(extension)
-
4
@trackers[handler] = tracker
-
end
-
-
2
def self.remove_tracker(handler)
-
@trackers.delete(handler)
-
end
-
-
2
class ERBTracker # :nodoc:
-
2
EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
-
-
# A valid ruby identifier - suitable for class, method and specially variable names
-
2
IDENTIFIER = /
-
[[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
-
[[:word:]]* # followed by optional letters, numbers or underscores
-
/x
-
-
# Any kind of variable name. e.g. @instance, @@class, $global or local.
-
# Possibly following a method call chain
-
2
VARIABLE_OR_METHOD_CHAIN = /
-
(?:\$|@{1,2})? # optional global, instance or class variable indicator
-
(?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
-
(?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
-
/x
-
-
# A simple string literal. e.g. "School's out!"
-
2
STRING = /
-
(?<quote>['"]) # an opening quote
-
(?<static>.*?) # with anything inside, captured as STATIC
-
\k<quote> # and a matching closing quote
-
/x
-
-
# Part of any hash containing the :partial key
-
2
PARTIAL_HASH_KEY = /
-
(?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
-
\s* # followed by optional spaces
-
/x
-
-
# Part of any hash containing the :layout key
-
2
LAYOUT_HASH_KEY = /
-
(?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
-
\s* # followed by optional spaces
-
/x
-
-
# Matches:
-
# partial: "comments/comment", collection: @all_comments => "comments/comment"
-
# (object: @single_comment, partial: "comments/comment") => "comments/comment"
-
#
-
# "comments/comments"
-
# 'comments/comments'
-
# ('comments/comments')
-
#
-
# (@topic) => "topics/topic"
-
# topics => "topics/topic"
-
# (message.topics) => "topics/topic"
-
2
RENDER_ARGUMENTS = /\A
-
(?:\s*\(?\s*) # optional opening paren surrounded by spaces
-
(?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
-
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
-
/xm
-
-
2
def self.call(name, template)
-
new(name, template).dependencies
-
end
-
-
2
def initialize(name, template)
-
@name, @template = name, template
-
end
-
-
2
def dependencies
-
render_dependencies + explicit_dependencies
-
end
-
-
2
attr_reader :name, :template
-
2
private :name, :template
-
-
-
2
private
-
2
def source
-
template.source
-
end
-
-
2
def directory
-
name.split("/")[0..-2].join("/")
-
end
-
-
2
def render_dependencies
-
render_dependencies = []
-
render_calls = source.split(/\brender\b/).drop(1)
-
-
render_calls.each do |arguments|
-
arguments.scan(RENDER_ARGUMENTS) do
-
add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
-
add_static_dependency(render_dependencies, Regexp.last_match[:static])
-
end
-
end
-
-
render_dependencies.uniq
-
end
-
-
2
def add_dynamic_dependency(dependencies, dependency)
-
if dependency
-
dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
-
end
-
end
-
-
2
def add_static_dependency(dependencies, dependency)
-
if dependency
-
if dependency.include?('/')
-
dependencies << dependency
-
else
-
dependencies << "#{directory}/#{dependency}"
-
end
-
end
-
end
-
-
2
def explicit_dependencies
-
source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
-
end
-
end
-
-
2
register_tracker :erb, ERBTracker
-
end
-
end
-
1
require 'active_support/core_ext/string/output_safety'
-
-
1
module ActionView
-
1
class OutputFlow #:nodoc:
-
1
attr_reader :content
-
-
1
def initialize
-
16
@content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
-
end
-
-
# Called by _layout_for to read stored values.
-
1
def get(key)
-
16
@content[key]
-
end
-
-
# Called by each renderer object to set the layout contents.
-
1
def set(key, value)
-
16
@content[key] = ActiveSupport::SafeBuffer.new(value)
-
end
-
-
# Called by content_for
-
1
def append(key, value)
-
@content[key] << value
-
end
-
1
alias_method :append!, :append
-
-
end
-
-
1
class StreamingFlow < OutputFlow #:nodoc:
-
1
def initialize(view, fiber)
-
@view = view
-
@parent = nil
-
@child = view.output_buffer
-
@content = view.view_flow.content
-
@fiber = fiber
-
@root = Fiber.current.object_id
-
end
-
-
# Try to get stored content. If the content
-
# is not available and we are inside the layout
-
# fiber, we set that we are waiting for the given
-
# key and yield.
-
1
def get(key)
-
return super if @content.key?(key)
-
-
if inside_fiber?
-
view = @view
-
-
begin
-
@waiting_for = key
-
view.output_buffer, @parent = @child, view.output_buffer
-
Fiber.yield
-
ensure
-
@waiting_for = nil
-
view.output_buffer, @child = @parent, view.output_buffer
-
end
-
end
-
-
super
-
end
-
-
# Appends the contents for the given key. This is called
-
# by provides and resumes back to the fiber if it is
-
# the key it is waiting for.
-
1
def append!(key, value)
-
super
-
@fiber.resume if @waiting_for == key
-
end
-
-
1
private
-
-
1
def inside_fiber?
-
Fiber.current.object_id != @root
-
end
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
1
module Tags #:nodoc:
-
1
extend ActiveSupport::Autoload
-
-
1
eager_autoload do
-
1
autoload :Base
-
1
autoload :Translator
-
1
autoload :CheckBox
-
1
autoload :CollectionCheckBoxes
-
1
autoload :CollectionRadioButtons
-
1
autoload :CollectionSelect
-
1
autoload :ColorField
-
1
autoload :DateField
-
1
autoload :DateSelect
-
1
autoload :DatetimeField
-
1
autoload :DatetimeLocalField
-
1
autoload :DatetimeSelect
-
1
autoload :EmailField
-
1
autoload :FileField
-
1
autoload :GroupedCollectionSelect
-
1
autoload :HiddenField
-
1
autoload :Label
-
1
autoload :MonthField
-
1
autoload :NumberField
-
1
autoload :PasswordField
-
1
autoload :RadioButton
-
1
autoload :RangeField
-
1
autoload :SearchField
-
1
autoload :Select
-
1
autoload :TelField
-
1
autoload :TextArea
-
1
autoload :TextField
-
1
autoload :TimeField
-
1
autoload :TimeSelect
-
1
autoload :TimeZoneSelect
-
1
autoload :UrlField
-
1
autoload :WeekField
-
end
-
end
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
class Base # :nodoc:
-
1
include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
-
1
include FormOptionsHelper
-
-
1
attr_reader :object
-
-
1
def initialize(object_name, method_name, template_object, options = {})
-
66
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
-
66
@template_object = template_object
-
-
66
@object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
-
66
@object = retrieve_object(options.delete(:object))
-
66
@options = options
-
66
@auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
-
end
-
-
# This is what child classes implement.
-
1
def render
-
raise NotImplementedError, "Subclasses must implement a render method"
-
end
-
-
1
private
-
-
1
def value(object)
-
22
object.public_send @method_name if object
-
end
-
-
1
def value_before_type_cast(object)
-
30
unless object.nil?
-
30
method_before_type_cast = @method_name + "_before_type_cast"
-
-
30
if value_came_from_user?(object) && object.respond_to?(method_before_type_cast)
-
14
object.public_send(method_before_type_cast)
-
else
-
16
value(object)
-
end
-
end
-
end
-
-
1
def value_came_from_user?(object)
-
30
method_name = "#{@method_name}_came_from_user?"
-
30
!object.respond_to?(method_name) || object.public_send(method_name)
-
end
-
-
1
def retrieve_object(object)
-
66
if object
-
66
object
-
elsif @template_object.instance_variable_defined?("@#{@object_name}")
-
@template_object.instance_variable_get("@#{@object_name}")
-
end
-
rescue NameError
-
# As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
-
nil
-
end
-
-
1
def retrieve_autoindex(pre_match)
-
object = self.object || @template_object.instance_variable_get("@#{pre_match}")
-
if object && object.respond_to?(:to_param)
-
object.to_param
-
else
-
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
-
end
-
end
-
-
1
def add_default_name_and_id_for_value(tag_value, options)
-
33
if tag_value.nil?
-
33
add_default_name_and_id(options)
-
else
-
specified_id = options["id"]
-
add_default_name_and_id(options)
-
-
if specified_id.blank? && options["id"].present?
-
options["id"] += "_#{sanitized_value(tag_value)}"
-
end
-
end
-
end
-
-
1
def add_default_name_and_id(options)
-
66
if options.has_key?("index")
-
options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) }
-
options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
-
options.delete("index")
-
elsif defined?(@auto_index)
-
options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) }
-
options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
-
else
-
132
options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) }
-
132
options["id"] = options.fetch("id"){ tag_id }
-
end
-
-
66
options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
-
end
-
-
1
def tag_name(multiple = false)
-
66
"#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
-
end
-
-
1
def tag_name_with_index(index, multiple = false)
-
"#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
-
end
-
-
1
def tag_id
-
66
"#{sanitized_object_name}_#{sanitized_method_name}"
-
end
-
-
1
def tag_id_with_index(index)
-
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
-
end
-
-
1
def sanitized_object_name
-
66
@sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
-
end
-
-
1
def sanitized_method_name
-
132
@sanitized_method_name ||= @method_name.sub(/\?$/,"")
-
end
-
-
1
def sanitized_value(value)
-
value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
-
end
-
-
1
def select_content_tag(option_tags, options, html_options)
-
3
html_options = html_options.stringify_keys
-
3
add_default_name_and_id(html_options)
-
3
options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options)
-
6
value = options.fetch(:selected) { value(object) }
-
3
select = content_tag("select", add_options(option_tags, options, value), html_options)
-
-
3
if html_options["multiple"] && options.fetch(:include_hidden, true)
-
tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select
-
else
-
3
select
-
end
-
end
-
-
1
def select_not_required?(html_options)
-
3
!html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1
-
end
-
-
1
def add_options(option_tags, options, value = nil)
-
3
if options[:include_blank]
-
3
option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags
-
end
-
3
if value.blank? && options[:prompt]
-
option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags
-
end
-
3
option_tags
-
end
-
end
-
end
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
class CollectionSelect < Base #:nodoc:
-
1
def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
-
3
@collection = collection
-
3
@value_method = value_method
-
3
@text_method = text_method
-
3
@html_options = html_options
-
-
3
super(object_name, method_name, template_object, options)
-
end
-
-
1
def render
-
3
option_tags_options = {
-
3
:selected => @options.fetch(:selected) { value(@object) },
-
:disabled => @options[:disabled]
-
}
-
-
3
select_content_tag(
-
options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options),
-
@options, @html_options
-
)
-
end
-
end
-
end
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
class Label < Base # :nodoc:
-
1
class LabelBuilder # :nodoc:
-
1
attr_reader :object
-
-
1
def initialize(template_object, object_name, method_name, object, tag_value)
-
33
@template_object = template_object
-
33
@object_name = object_name
-
33
@method_name = method_name
-
33
@object = object
-
33
@tag_value = tag_value
-
end
-
-
1
def translation
-
33
method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name
-
-
content ||= Translator
-
.new(object, @object_name, method_and_value, "helpers.label")
-
33
.translate
-
33
content ||= @method_name.humanize
-
-
33
content
-
end
-
end
-
-
1
def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
-
33
options ||= {}
-
-
33
content_is_options = content_or_options.is_a?(Hash)
-
33
if content_is_options
-
options.merge! content_or_options
-
@content = nil
-
else
-
33
@content = content_or_options
-
end
-
-
33
super(object_name, method_name, template_object, options)
-
end
-
-
1
def render(&block)
-
33
options = @options.stringify_keys
-
33
tag_value = options.delete("value")
-
33
name_and_id = options.dup
-
-
33
if name_and_id["for"]
-
name_and_id["id"] = name_and_id["for"]
-
else
-
33
name_and_id.delete("id")
-
end
-
-
33
add_default_name_and_id_for_value(tag_value, name_and_id)
-
33
options.delete("index")
-
33
options.delete("namespace")
-
33
options["for"] = name_and_id["id"] unless options.key?("for")
-
-
33
builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)
-
-
33
content = if block_given?
-
@template_object.capture(builder, &block)
-
elsif @content.present?
-
@content.to_s
-
else
-
33
render_component(builder)
-
end
-
-
33
label_tag(name_and_id["id"], content, options)
-
end
-
-
1
private
-
-
1
def render_component(builder)
-
33
builder.translation
-
end
-
end
-
end
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
module Placeholderable # :nodoc:
-
1
def initialize(*)
-
30
super
-
-
30
if tag_value = @options[:placeholder]
-
26
placeholder = tag_value if tag_value.is_a?(String)
-
26
method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
-
-
placeholder ||= Tags::Translator
-
.new(object, @object_name, method_and_value, "helpers.placeholder")
-
26
.translate
-
26
placeholder ||= @method_name.humanize
-
26
@options[:placeholder] = placeholder
-
end
-
end
-
end
-
end
-
end
-
end
-
1
require 'action_view/helpers/tags/placeholderable'
-
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
class TextField < Base # :nodoc:
-
1
include Placeholderable
-
-
1
def render
-
30
options = @options.stringify_keys
-
30
options["size"] = options["maxlength"] unless options.key?("size")
-
30
options["type"] ||= field_type
-
60
options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file"
-
30
yield options if block_given?
-
30
add_default_name_and_id(options)
-
30
tag("input", options)
-
end
-
-
1
class << self
-
1
def field_type
-
60
@field_type ||= self.name.split("::").last.sub("Field", "").downcase
-
end
-
end
-
-
1
private
-
-
1
def field_type
-
60
self.class.field_type
-
end
-
end
-
end
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
1
module Tags # :nodoc:
-
1
class Translator # :nodoc:
-
1
def initialize(object, object_name, method_and_value, scope)
-
33
@object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
-
33
@method_and_value = method_and_value
-
33
@scope = scope
-
33
@model = object.respond_to?(:to_model) ? object.to_model : nil
-
end
-
-
1
def translate
-
33
translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
-
33
translated_attribute || human_attribute_name
-
end
-
-
1
protected
-
-
1
attr_reader :object_name, :method_and_value, :scope, :model
-
-
1
private
-
-
1
def i18n_default
-
33
if model
-
33
key = model.model_name.i18n_key
-
33
["#{key}.#{method_and_value}".to_sym, ""]
-
else
-
""
-
end
-
end
-
-
1
def human_attribute_name
-
33
if model && model.class.respond_to?(:human_attribute_name)
-
33
model.class.human_attribute_name(method_and_value)
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require "action_view/rendering"
-
2
require "active_support/core_ext/module/remove_method"
-
-
2
module ActionView
-
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
-
# repeated setups. The inclusion pattern has pages that look like this:
-
#
-
# <%= render "shared/header" %>
-
# Hello World
-
# <%= render "shared/footer" %>
-
#
-
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
-
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
-
#
-
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
-
# that the header and footer are only mentioned in one place, like this:
-
#
-
# // The header part of this layout
-
# <%= yield %>
-
# // The footer part of this layout
-
#
-
# And then you have content pages that look like this:
-
#
-
# hello world
-
#
-
# At rendering time, the content page is computed and then inserted in the layout, like this:
-
#
-
# // The header part of this layout
-
# hello world
-
# // The footer part of this layout
-
#
-
# == Accessing shared variables
-
#
-
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
-
# references that won't materialize before rendering time:
-
#
-
# <h1><%= @page_title %></h1>
-
# <%= yield %>
-
#
-
# ...and content pages that fulfill these references _at_ rendering time:
-
#
-
# <% @page_title = "Welcome" %>
-
# Off-world colonies offers you a chance to start a new life
-
#
-
# The result after rendering is:
-
#
-
# <h1>Welcome</h1>
-
# Off-world colonies offers you a chance to start a new life
-
#
-
# == Layout assignment
-
#
-
# You can either specify a layout declaratively (using the #layout class method) or give
-
# it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
-
# If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
-
#
-
# For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
-
# that template will be used for all actions in PostsController and controllers inheriting
-
# from PostsController.
-
#
-
# If you use a module, for instance Weblog::PostsController, you will need a template named
-
# <tt>app/views/layouts/weblog/posts.html.erb</tt>.
-
#
-
# Since all your controllers inherit from ApplicationController, they will use
-
# <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
-
# or provided.
-
#
-
# == Inheritance Examples
-
#
-
# class BankController < ActionController::Base
-
# # bank.html.erb exists
-
#
-
# class ExchangeController < BankController
-
# # exchange.html.erb exists
-
#
-
# class CurrencyController < BankController
-
#
-
# class InformationController < BankController
-
# layout "information"
-
#
-
# class TellerController < InformationController
-
# # teller.html.erb exists
-
#
-
# class EmployeeController < InformationController
-
# # employee.html.erb exists
-
# layout nil
-
#
-
# class VaultController < BankController
-
# layout :access_level_layout
-
#
-
# class TillController < BankController
-
# layout false
-
#
-
# In these examples, we have three implicit lookup scenarios:
-
# * The BankController uses the "bank" layout.
-
# * The ExchangeController uses the "exchange" layout.
-
# * The CurrencyController inherits the layout from BankController.
-
#
-
# However, when a layout is explicitly set, the explicitly set layout wins:
-
# * The InformationController uses the "information" layout, explicitly set.
-
# * The TellerController also uses the "information" layout, because the parent explicitly set it.
-
# * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration.
-
# * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
-
# * The TillController does not use a layout at all.
-
#
-
# == Types of layouts
-
#
-
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
-
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
-
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
-
#
-
# The method reference is the preferred approach to variable layouts and is used like this:
-
#
-
# class WeblogController < ActionController::Base
-
# layout :writers_and_readers
-
#
-
# def index
-
# # fetching posts
-
# end
-
#
-
# private
-
# def writers_and_readers
-
# logged_in? ? "writer_layout" : "reader_layout"
-
# end
-
# end
-
#
-
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
-
# is logged in or not.
-
#
-
# If you want to use an inline method, such as a proc, do something like this:
-
#
-
# class WeblogController < ActionController::Base
-
# layout proc { |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
-
# end
-
#
-
# If an argument isn't given to the proc, it's evaluated in the context of
-
# the current controller anyway.
-
#
-
# class WeblogController < ActionController::Base
-
# layout proc { logged_in? ? "writer_layout" : "reader_layout" }
-
# end
-
#
-
# Of course, the most common way of specifying a layout is still just as a plain template name:
-
#
-
# class WeblogController < ActionController::Base
-
# layout "weblog_standard"
-
# end
-
#
-
# The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
-
# <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
-
#
-
# Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
-
# Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent:
-
#
-
# class ApplicationController < ActionController::Base
-
# layout "application"
-
# end
-
#
-
# class PostsController < ApplicationController
-
# # Will use "application" layout
-
# end
-
#
-
# class CommentsController < ApplicationController
-
# # Will search for "comments" layout and fallback "application" layout
-
# layout nil
-
# end
-
#
-
# == Conditional layouts
-
#
-
# If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
-
# a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
-
# <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
-
#
-
# class WeblogController < ActionController::Base
-
# layout "weblog_standard", except: :rss
-
#
-
# # ...
-
#
-
# end
-
#
-
# This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will
-
# be rendered directly, without wrapping a layout around the rendered view.
-
#
-
# Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
-
# #<tt>except: [ :rss, :text_only ]</tt> is valid, as is <tt>except: :rss</tt>.
-
#
-
# == Using a different layout in the action render call
-
#
-
# If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
-
# Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
-
# You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
-
#
-
# class WeblogController < ActionController::Base
-
# layout "weblog_standard"
-
#
-
# def help
-
# render action: "help", layout: "help"
-
# end
-
# end
-
#
-
# This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead.
-
2
module Layouts
-
2
extend ActiveSupport::Concern
-
-
2
include ActionView::Rendering
-
-
2
included do
-
4
class_attribute :_layout, :_layout_conditions, :instance_accessor => false
-
4
self._layout = nil
-
4
self._layout_conditions = {}
-
4
_write_layout_method
-
end
-
-
2
delegate :_layout_conditions, to: :class
-
-
2
module ClassMethods
-
2
def inherited(klass) # :nodoc:
-
8
super
-
8
klass._write_layout_method
-
end
-
-
# This module is mixed in if layout conditions are provided. This means
-
# that if no layout conditions are used, this method is not used
-
2
module LayoutConditions # :nodoc:
-
2
private
-
-
# Determines whether the current action has a layout definition by
-
# checking the action name against the :only and :except conditions
-
# set by the <tt>layout</tt> method.
-
#
-
# ==== Returns
-
# * <tt> Boolean</tt> - True if the action has a layout definition, false otherwise.
-
2
def _conditional_layout?
-
return unless super
-
-
conditions = _layout_conditions
-
-
if only = conditions[:only]
-
only.include?(action_name)
-
elsif except = conditions[:except]
-
!except.include?(action_name)
-
else
-
true
-
end
-
end
-
end
-
-
# Specify the layout to use for this class.
-
#
-
# If the specified layout is a:
-
# String:: the String is the template name
-
# Symbol:: call the method specified by the symbol, which will return the template name
-
# false:: There is no layout
-
# true:: raise an ArgumentError
-
# nil:: Force default layout behavior with inheritance
-
#
-
# ==== Parameters
-
# * <tt>layout</tt> - The layout to use.
-
#
-
# ==== Options (conditions)
-
# * :only - A list of actions to apply this layout to.
-
# * :except - Apply this layout to all actions but this one.
-
2
def layout(layout, conditions = {})
-
include LayoutConditions unless conditions.empty?
-
-
conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} }
-
self._layout_conditions = conditions
-
-
self._layout = layout
-
_write_layout_method
-
end
-
-
# Creates a _layout method to be called by _default_layout .
-
#
-
# If a layout is not explicitly mentioned then look for a layout with the controller's name.
-
# if nothing is found then try same procedure to find super class's layout.
-
2
def _write_layout_method # :nodoc:
-
12
remove_possible_method(:_layout)
-
-
12
prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
-
12
default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super"
-
12
name_clause = if name
-
12
default_behavior
-
else
-
<<-RUBY
-
super
-
RUBY
-
end
-
-
12
layout_definition = case _layout
-
when String
-
_layout.inspect
-
when Symbol
-
<<-RUBY
-
#{_layout}.tap do |layout|
-
return #{default_behavior} if layout.nil?
-
unless layout.is_a?(String) || !layout
-
raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \
-
"should have returned a String, false, or nil"
-
end
-
end
-
RUBY
-
when Proc
-
define_method :_layout_from_proc, &_layout
-
protected :_layout_from_proc
-
<<-RUBY
-
result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'})
-
return #{default_behavior} if result.nil?
-
result
-
RUBY
-
when false
-
nil
-
when true
-
raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil"
-
when nil
-
12
name_clause
-
end
-
-
12
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def _layout
-
if _conditional_layout?
-
#{layout_definition}
-
else
-
#{name_clause}
-
end
-
end
-
private :_layout
-
RUBY
-
end
-
-
2
private
-
-
# If no layout is supplied, look for a template named the return
-
# value of this method.
-
#
-
# ==== Returns
-
# * <tt>String</tt> - A template name
-
2
def _implied_layout_name # :nodoc:
-
24
controller_path
-
end
-
end
-
-
2
def _normalize_options(options) # :nodoc:
-
16
super
-
-
16
if _include_layout?(options)
-
32
layout = options.delete(:layout) { :default }
-
16
options[:layout] = _layout_for_option(layout)
-
end
-
end
-
-
2
attr_internal_writer :action_has_layout
-
-
2
def initialize(*) # :nodoc:
-
29
@_action_has_layout = true
-
29
super
-
end
-
-
# Controls whether an action should be rendered using a layout.
-
# If you want to disable any <tt>layout</tt> settings for the
-
# current action so that it is rendered without a layout then
-
# either override this method in your controller to return false
-
# for that action or set the <tt>action_has_layout</tt> attribute
-
# to false before rendering.
-
2
def action_has_layout?
-
16
@_action_has_layout
-
end
-
-
2
private
-
-
2
def _conditional_layout?
-
32
true
-
end
-
-
# This will be overwritten by _write_layout_method
-
2
def _layout; end
-
-
# Determine the layout for a given name, taking into account the name type.
-
#
-
# ==== Parameters
-
# * <tt>name</tt> - The name of the template
-
2
def _layout_for_option(name)
-
16
case name
-
when String then _normalize_layout(name)
-
when Proc then name
-
when true then Proc.new { _default_layout(true) }
-
32
when :default then Proc.new { _default_layout(false) }
-
when false, nil then nil
-
else
-
raise ArgumentError,
-
"String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}"
-
end
-
end
-
-
2
def _normalize_layout(value)
-
16
value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value
-
end
-
-
# Returns the default layout for this controller.
-
# Optionally raises an exception if the layout could not be found.
-
#
-
# ==== Parameters
-
# * <tt>require_layout</tt> - If set to true and layout is not found,
-
# an ArgumentError exception is raised (defaults to false)
-
#
-
# ==== Returns
-
# * <tt>template</tt> - The template object for the default layout (or nil)
-
2
def _default_layout(require_layout = false)
-
16
begin
-
16
value = _layout if action_has_layout?
-
rescue NameError => e
-
raise e, "Could not render layout: #{e.message}"
-
end
-
-
16
if require_layout && action_has_layout? && !value
-
raise ArgumentError,
-
"There was no default layout for #{self.class} in #{view_paths.inspect}"
-
end
-
-
16
_normalize_layout(value)
-
end
-
-
2
def _include_layout?(options)
-
16
(options.keys & [:body, :text, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
-
end
-
end
-
end
-
2
module ActionView #:nodoc:
-
# = Action View PathSet
-
#
-
# This class is used to store and access paths in Action View. A number of
-
# operations are defined so that you can search among the paths in this
-
# set and also perform operations on other +PathSet+ objects.
-
#
-
# A +LookupContext+ will use a +PathSet+ to store the paths in its context.
-
2
class PathSet #:nodoc:
-
2
include Enumerable
-
-
2
attr_reader :paths
-
-
2
delegate :[], :include?, :pop, :size, :each, to: :paths
-
-
2
def initialize(paths = [])
-
39
@paths = typecast paths
-
end
-
-
2
def initialize_copy(other)
-
@paths = other.paths.dup
-
self
-
end
-
-
2
def to_ary
-
35
paths.dup
-
end
-
-
2
def compact
-
PathSet.new paths.compact
-
end
-
-
2
def +(array)
-
PathSet.new(paths + array)
-
end
-
-
2
%w(<< concat push insert unshift).each do |method|
-
10
class_eval <<-METHOD, __FILE__, __LINE__ + 1
-
def #{method}(*args)
-
paths.#{method}(*typecast(args))
-
end
-
METHOD
-
end
-
-
2
def find(*args)
-
26
find_all(*args).first || raise(MissingTemplate.new(self, *args))
-
end
-
-
2
def find_file(path, prefixes = [], *args)
-
_find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
-
end
-
-
2
def find_all(path, prefixes = [], *args)
-
58
_find_all path, prefixes, args, false
-
end
-
-
2
def exists?(path, prefixes, *args)
-
find_all(path, prefixes, *args).any?
-
end
-
-
2
private
-
-
2
def _find_all(path, prefixes, args, outside_app)
-
58
prefixes = [prefixes] if String === prefixes
-
58
prefixes.each do |prefix|
-
58
paths.each do |resolver|
-
74
if outside_app
-
templates = resolver.find_all_anywhere(path, prefix, *args)
-
else
-
74
templates = resolver.find_all(path, prefix, *args)
-
end
-
74
return templates unless templates.empty?
-
end
-
end
-
16
[]
-
end
-
-
2
def typecast(paths)
-
39
paths.map do |path|
-
66
case path
-
when Pathname, String
-
8
OptimizedFileSystemResolver.new path.to_s
-
else
-
58
path
-
end
-
end
-
end
-
end
-
end
-
1
module ActionView
-
# This class defines the interface for a renderer. Each class that
-
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
-
# render a specific type of object.
-
#
-
# The base +Renderer+ class uses its +render+ method to delegate to the
-
# renderers. These currently consist of
-
#
-
# PartialRenderer - Used for rendering partials
-
# TemplateRenderer - Used for rendering other types of templates
-
# StreamingTemplateRenderer - Used for streaming
-
#
-
# Whenever the +render+ method is called on the base +Renderer+ class, a new
-
# renderer object of the correct type is created, and the +render+ method on
-
# that new object is called in turn. This abstracts the setup and rendering
-
# into a separate classes for partials and templates.
-
1
class AbstractRenderer #:nodoc:
-
1
delegate :find_template, :find_file, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
-
-
1
def initialize(lookup_context)
-
26
@lookup_context = lookup_context
-
end
-
-
1
def render
-
raise NotImplementedError
-
end
-
-
1
protected
-
-
1
def extract_details(options)
-
26
@lookup_context.registered_details.each_with_object({}) do |key, details|
-
104
value = options[key]
-
-
104
details[key] = Array(value) if value
-
end
-
end
-
-
1
def instrument(name, options={})
-
52
ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
-
end
-
-
1
def prepend_formats(formats)
-
26
formats = Array(formats)
-
26
return if formats.empty? || @lookup_context.html_fallback_for_js
-
-
16
@lookup_context.formats = formats | @lookup_context.formats
-
end
-
end
-
end
-
1
require 'thread_safe'
-
-
1
module ActionView
-
1
class PartialIteration
-
# The number of iterations that will be done by the partial.
-
1
attr_reader :size
-
-
# The current iteration of the partial.
-
1
attr_reader :index
-
-
1
def initialize(size)
-
@size = size
-
@index = 0
-
end
-
-
# Check if this is the first iteration of the partial.
-
1
def first?
-
index == 0
-
end
-
-
# Check if this is the last iteration of the partial.
-
1
def last?
-
index == size - 1
-
end
-
-
1
def iterate! # :nodoc:
-
@index += 1
-
end
-
end
-
-
# = Action View Partials
-
#
-
# There's also a convenience method for rendering sub templates within the current controller that depends on a
-
# single object (we call this kind of sub templates for partials). It relies on the fact that partials should
-
# follow the naming convention of being prefixed with an underscore -- as to separate them from regular
-
# templates that could be rendered on their own.
-
#
-
# In a template for Advertiser#account:
-
#
-
# <%= render partial: "account" %>
-
#
-
# This would render "advertiser/_account.html.erb".
-
#
-
# In another template for Advertiser#buy, we could have:
-
#
-
# <%= render partial: "account", locals: { account: @buyer } %>
-
#
-
# <% @advertisements.each do |ad| %>
-
# <%= render partial: "ad", locals: { ad: ad } %>
-
# <% end %>
-
#
-
# This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then
-
# render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display.
-
#
-
# == The :as and :object options
-
#
-
# By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables.
-
# The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
-
#
-
# <%= render partial: "account", object: @buyer %>
-
#
-
# would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
-
# equivalent to:
-
#
-
# <%= render partial: "account", locals: { account: @buyer } %>
-
#
-
# With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
-
# wanted it to be +user+ instead of +account+ we'd do:
-
#
-
# <%= render partial: "account", object: @buyer, as: 'user' %>
-
#
-
# This is equivalent to
-
#
-
# <%= render partial: "account", locals: { user: @buyer } %>
-
#
-
# == Rendering a collection of partials
-
#
-
# The example of partial use describes a familiar pattern where a template needs to iterate over an array and
-
# render a sub template for each of the elements. This pattern has been implemented as a single method that
-
# accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
-
# example in "Using partials" can be rewritten with a single line:
-
#
-
# <%= render partial: "ad", collection: @advertisements %>
-
#
-
# This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An
-
# iteration object will automatically be made available to the template with a name of the form
-
# +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
-
# the collection and the total size of the collection. The iteration object also has two convenience methods,
-
# +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
-
# For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
-
# +index+ method.
-
#
-
# The <tt>:as</tt> option may be used when rendering partials.
-
#
-
# You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
-
# The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
-
#
-
# <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
-
#
-
# If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you
-
# to specify a text which will displayed instead by using this form:
-
#
-
# <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
-
#
-
# NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
-
# just keep domain objects, like Active Records, in there.
-
#
-
# == Rendering shared partials
-
#
-
# Two controllers can share a set of partials and render them like this:
-
#
-
# <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
-
#
-
# This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from.
-
#
-
# == Rendering objects that respond to `to_partial_path`
-
#
-
# Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
-
# and pick the proper path by checking `to_partial_path` method.
-
#
-
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
-
# # <%= render partial: "accounts/account", locals: { account: @account} %>
-
# <%= render partial: @account %>
-
#
-
# # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
-
# # that's why we can replace:
-
# # <%= render partial: "posts/post", collection: @posts %>
-
# <%= render partial: @posts %>
-
#
-
# == Rendering the default case
-
#
-
# If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
-
# defaults of render to render partials. Examples:
-
#
-
# # Instead of <%= render partial: "account" %>
-
# <%= render "account" %>
-
#
-
# # Instead of <%= render partial: "account", locals: { account: @buyer } %>
-
# <%= render "account", account: @buyer %>
-
#
-
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
-
# # <%= render partial: "accounts/account", locals: { account: @account} %>
-
# <%= render @account %>
-
#
-
# # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
-
# # that's why we can replace:
-
# # <%= render partial: "posts/post", collection: @posts %>
-
# <%= render @posts %>
-
#
-
# == Rendering partials with layouts
-
#
-
# Partials can have their own layouts applied to them. These layouts are different than the ones that are
-
# specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
-
# of users:
-
#
-
# <%# app/views/users/index.html.erb &>
-
# Here's the administrator:
-
# <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
-
#
-
# Here's the editor:
-
# <%= render partial: "user", layout: "editor", locals: { user: editor } %>
-
#
-
# <%# app/views/users/_user.html.erb &>
-
# Name: <%= user.name %>
-
#
-
# <%# app/views/users/_administrator.html.erb &>
-
# <div id="administrator">
-
# Budget: $<%= user.budget %>
-
# <%= yield %>
-
# </div>
-
#
-
# <%# app/views/users/_editor.html.erb &>
-
# <div id="editor">
-
# Deadline: <%= user.deadline %>
-
# <%= yield %>
-
# </div>
-
#
-
# ...this will return:
-
#
-
# Here's the administrator:
-
# <div id="administrator">
-
# Budget: $<%= user.budget %>
-
# Name: <%= user.name %>
-
# </div>
-
#
-
# Here's the editor:
-
# <div id="editor">
-
# Deadline: <%= user.deadline %>
-
# Name: <%= user.name %>
-
# </div>
-
#
-
# If a collection is given, the layout will be rendered once for each item in
-
# the collection. For example, these two snippets have the same output:
-
#
-
# <%# app/views/users/_user.html.erb %>
-
# Name: <%= user.name %>
-
#
-
# <%# app/views/users/index.html.erb %>
-
# <%# This does not use layouts %>
-
# <ul>
-
# <% users.each do |user| -%>
-
# <li>
-
# <%= render partial: "user", locals: { user: user } %>
-
# </li>
-
# <% end -%>
-
# </ul>
-
#
-
# <%# app/views/users/_li_layout.html.erb %>
-
# <li>
-
# <%= yield %>
-
# </li>
-
#
-
# <%# app/views/users/index.html.erb %>
-
# <ul>
-
# <%= render partial: "user", layout: "li_layout", collection: users %>
-
# </ul>
-
#
-
# Given two users whose names are Alice and Bob, these snippets return:
-
#
-
# <ul>
-
# <li>
-
# Name: Alice
-
# </li>
-
# <li>
-
# Name: Bob
-
# </li>
-
# </ul>
-
#
-
# The current object being rendered, as well as the object_counter, will be
-
# available as local variables inside the layout template under the same names
-
# as available in the partial.
-
#
-
# You can also apply a layout to a block within any template:
-
#
-
# <%# app/views/users/_chief.html.erb &>
-
# <%= render(layout: "administrator", locals: { user: chief }) do %>
-
# Title: <%= chief.title %>
-
# <% end %>
-
#
-
# ...this will return:
-
#
-
# <div id="administrator">
-
# Budget: $<%= user.budget %>
-
# Title: <%= chief.name %>
-
# </div>
-
#
-
# As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
-
#
-
# If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
-
# an array to layout and treat it as an enumerable.
-
#
-
# <%# app/views/users/_user.html.erb &>
-
# <div class="user">
-
# Budget: $<%= user.budget %>
-
# <%= yield user %>
-
# </div>
-
#
-
# <%# app/views/users/index.html.erb &>
-
# <%= render layout: @users do |user| %>
-
# Title: <%= user.title %>
-
# <% end %>
-
#
-
# This will render the layout for each user and yield to the block, passing the user, each time.
-
#
-
# You can also yield multiple times in one layout and use block arguments to differentiate the sections.
-
#
-
# <%# app/views/users/_user.html.erb &>
-
# <div class="user">
-
# <%= yield user, :header %>
-
# Budget: $<%= user.budget %>
-
# <%= yield user, :footer %>
-
# </div>
-
#
-
# <%# app/views/users/index.html.erb &>
-
# <%= render layout: @users do |user, section| %>
-
# <%- case section when :header -%>
-
# Title: <%= user.title %>
-
# <%- when :footer -%>
-
# Deadline: <%= user.deadline %>
-
# <%- end -%>
-
# <% end %>
-
1
class PartialRenderer < AbstractRenderer
-
1
PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
-
h[k] = ThreadSafe::Cache.new
-
end
-
-
1
def initialize(*)
-
10
super
-
10
@context_prefix = @lookup_context.prefixes.first
-
end
-
-
1
def render(context, options, block)
-
10
setup(context, options, block)
-
10
identifier = (@template = find_partial) ? @template.identifier : @path
-
-
10
@lookup_context.rendered_format ||= begin
-
if @template && @template.formats.present?
-
@template.formats.first
-
else
-
formats.first
-
end
-
end
-
-
10
if @collection
-
instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
-
render_collection
-
end
-
else
-
10
instrument(:partial, :identifier => identifier) do
-
10
render_partial
-
end
-
end
-
end
-
-
1
private
-
-
1
def render_collection
-
return nil if @collection.blank?
-
-
if @options.key?(:spacer_template)
-
spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
-
end
-
-
result = @template ? collection_with_template : collection_without_template
-
result.join(spacer).html_safe
-
end
-
-
1
def render_partial
-
10
view, locals, block = @view, @locals, @block
-
10
object, as = @object, @variable
-
-
10
if !block && (layout = @options[:layout])
-
layout = find_template(layout.to_s, @template_keys)
-
end
-
-
10
object = locals[as] if object.nil? # Respect object when object is false
-
10
locals[as] = object
-
-
10
content = @template.render(view, locals) do |*name|
-
view._layout_for(*name, &block)
-
end
-
-
10
content = layout.render(view, locals){ content } if layout
-
10
content
-
end
-
-
1
private
-
-
# Sets up instance variables needed for rendering a partial. This method
-
# finds the options and details and extracts them. The method also contains
-
# logic that handles the type of object passed in as the partial.
-
#
-
# If +options[:partial]+ is a string, then the +@path+ instance variable is
-
# set to that string. Otherwise, the +options[:partial]+ object must
-
# respond to +to_partial_path+ in order to setup the path.
-
1
def setup(context, options, block)
-
10
@view = context
-
10
@options = options
-
10
@block = block
-
-
10
@locals = options[:locals] || {}
-
10
@details = extract_details(options)
-
-
10
prepend_formats(options[:formats])
-
-
10
partial = options[:partial]
-
-
10
if String === partial
-
10
@has_object = options.key?(:object)
-
10
@object = options[:object]
-
10
@collection = collection_from_options
-
10
@path = partial
-
else
-
@has_object = true
-
@object = partial
-
@collection = collection_from_object || collection_from_options
-
-
if @collection
-
paths = @collection_data = @collection.map { |o| partial_path(o) }
-
@path = paths.uniq.one? ? paths.first : nil
-
else
-
@path = partial_path
-
end
-
end
-
-
10
if as = options[:as]
-
raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/
-
as = as.to_sym
-
end
-
-
10
if @path
-
10
@variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
-
10
@template_keys = retrieve_template_keys
-
else
-
paths.map! { |path| retrieve_variable(path, as).unshift(path) }
-
end
-
-
10
self
-
end
-
-
1
def collection_from_options
-
10
if @options.key?(:collection)
-
collection = @options[:collection]
-
collection.respond_to?(:to_ary) ? collection.to_ary : []
-
end
-
end
-
-
1
def collection_from_object
-
@object.to_ary if @object.respond_to?(:to_ary)
-
end
-
-
1
def find_partial
-
10
find_template(@path, @template_keys) if @path
-
end
-
-
1
def find_template(path, locals)
-
10
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
-
10
@lookup_context.find_template(path, prefixes, true, locals, @details)
-
end
-
-
1
def collection_with_template
-
view, locals, template = @view, @locals, @template
-
as, counter, iteration = @variable, @variable_counter, @variable_iteration
-
-
if layout = @options[:layout]
-
layout = find_template(layout, @template_keys)
-
end
-
-
partial_iteration = PartialIteration.new(@collection.size)
-
locals[iteration] = partial_iteration
-
-
@collection.map do |object|
-
locals[as] = object
-
locals[counter] = partial_iteration.index
-
-
content = template.render(view, locals)
-
content = layout.render(view, locals) { content } if layout
-
partial_iteration.iterate!
-
content
-
end
-
end
-
-
1
def collection_without_template
-
view, locals, collection_data = @view, @locals, @collection_data
-
cache = {}
-
keys = @locals.keys
-
-
partial_iteration = PartialIteration.new(@collection.size)
-
-
@collection.map do |object|
-
index = partial_iteration.index
-
path, as, counter, iteration = collection_data[index]
-
-
locals[as] = object
-
locals[counter] = index
-
locals[iteration] = partial_iteration
-
-
template = (cache[path] ||= find_template(path, keys + [as, counter]))
-
content = template.render(view, locals)
-
partial_iteration.iterate!
-
content
-
end
-
end
-
-
# Obtains the path to where the object's partial is located. If the object
-
# responds to +to_partial_path+, then +to_partial_path+ will be called and
-
# will provide the path. If the object does not respond to +to_partial_path+,
-
# then an +ArgumentError+ is raised.
-
#
-
# If +prefix_partial_path_with_controller_namespace+ is true, then this
-
# method will prefix the partial paths with a namespace.
-
1
def partial_path(object = @object)
-
object = object.to_model if object.respond_to?(:to_model)
-
-
path = if object.respond_to?(:to_partial_path)
-
object.to_partial_path
-
else
-
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
-
end
-
-
if @view.prefix_partial_path_with_controller_namespace
-
prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
-
else
-
path
-
end
-
end
-
-
1
def prefixed_partial_names
-
@prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
-
end
-
-
1
def merge_prefix_into_object_path(prefix, object_path)
-
if prefix.include?(?/) && object_path.include?(?/)
-
prefixes = []
-
prefix_array = File.dirname(prefix).split('/')
-
object_path_array = object_path.split('/')[0..-3] # skip model dir & partial
-
-
prefix_array.each_with_index do |dir, index|
-
break if dir == object_path_array[index]
-
prefixes << dir
-
end
-
-
(prefixes << object_path).join("/")
-
else
-
object_path
-
end
-
end
-
-
1
def retrieve_template_keys
-
10
keys = @locals.keys
-
10
keys << @variable if @has_object || @collection
-
10
if @collection
-
keys << @variable_counter
-
keys << @variable_iteration
-
end
-
10
keys
-
end
-
-
1
def retrieve_variable(path, as)
-
10
variable = as || begin
-
10
base = path[-1] == "/" ? "" : File.basename(path)
-
10
raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/
-
10
$1.to_sym
-
end
-
10
if @collection
-
variable_counter = :"#{variable}_counter"
-
variable_iteration = :"#{variable}_iteration"
-
end
-
10
[variable, variable_counter, variable_iteration]
-
end
-
-
1
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
-
"make sure your partial name starts with underscore, " +
-
"and is followed by any combination of letters, numbers and underscores."
-
-
1
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " +
-
"make sure it starts with lowercase letter, " +
-
"and is followed by any combination of letters, numbers and underscores."
-
-
1
def raise_invalid_identifier(path)
-
raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
-
end
-
-
1
def raise_invalid_option_as(as)
-
raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
-
end
-
end
-
end
-
1
module ActionView
-
# This is the main entry point for rendering. It basically delegates
-
# to other objects like TemplateRenderer and PartialRenderer which
-
# actually renders the template.
-
#
-
# The Renderer will parse the options from the +render+ or +render_body+
-
# method and render a partial or a template based on the options. The
-
# +TemplateRenderer+ and +PartialRenderer+ objects are wrappers which do all
-
# the setup and logic necessary to render a view and a new object is created
-
# each time +render+ is called.
-
1
class Renderer
-
1
attr_accessor :lookup_context
-
-
1
def initialize(lookup_context)
-
16
@lookup_context = lookup_context
-
end
-
-
# Main render entry point shared by AV and AC.
-
1
def render(context, options)
-
16
if options.respond_to?(:permitted?) && !options.permitted?
-
raise ArgumentError, "render parameters are not permitted"
-
end
-
-
16
if options.key?(:partial)
-
render_partial(context, options)
-
else
-
16
render_template(context, options)
-
end
-
end
-
-
# Render but returns a valid Rack body. If fibers are defined, we return
-
# a streaming body that renders the template piece by piece.
-
#
-
# Note that partials are not supported to be rendered with streaming,
-
# so in such cases, we just wrap them in an array.
-
1
def render_body(context, options)
-
if options.key?(:partial)
-
[render_partial(context, options)]
-
else
-
StreamingTemplateRenderer.new(@lookup_context).render(context, options)
-
end
-
end
-
-
# Direct accessor to template rendering.
-
1
def render_template(context, options) #:nodoc:
-
16
TemplateRenderer.new(@lookup_context).render(context, options)
-
end
-
-
# Direct access to partial rendering.
-
1
def render_partial(context, options, &block) #:nodoc:
-
10
PartialRenderer.new(@lookup_context).render(context, options, block)
-
end
-
end
-
end
-
1
require 'active_support/core_ext/object/try'
-
-
1
module ActionView
-
1
class TemplateRenderer < AbstractRenderer #:nodoc:
-
1
def render(context, options)
-
16
@view = context
-
16
@details = extract_details(options)
-
16
template = determine_template(options)
-
-
16
prepend_formats(template.formats)
-
-
16
@lookup_context.rendered_format ||= (template.formats.first || formats.first)
-
-
16
render_template(template, options[:layout], options[:locals])
-
end
-
-
1
private
-
-
# Determine the template to be rendered using the given options.
-
1
def determine_template(options)
-
16
keys = options.has_key?(:locals) ? options[:locals].keys : []
-
-
16
if options.key?(:body)
-
Template::Text.new(options[:body])
-
16
elsif options.key?(:text)
-
Template::Text.new(options[:text], formats.first)
-
16
elsif options.key?(:plain)
-
Template::Text.new(options[:plain])
-
16
elsif options.key?(:html)
-
Template::HTML.new(options[:html], formats.first)
-
16
elsif options.key?(:file)
-
with_fallbacks { find_file(options[:file], nil, false, keys, @details) }
-
16
elsif options.key?(:inline)
-
handler = Template.handler_for_extension(options[:type] || "erb")
-
Template.new(options[:inline], "inline template", handler, :locals => keys)
-
16
elsif options.key?(:template)
-
16
if options[:template].respond_to?(:render)
-
options[:template]
-
else
-
16
find_template(options[:template], options[:prefixes], false, keys, @details)
-
end
-
else
-
raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option."
-
end
-
end
-
-
# Renders the given template. A string representing the layout can be
-
# supplied as well.
-
1
def render_template(template, layout_name = nil, locals = nil) #:nodoc:
-
16
view, locals = @view, locals || {}
-
-
16
render_with_layout(layout_name, locals) do |layout|
-
16
instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
-
16
template.render(view, locals) { |*name| view._layout_for(*name) }
-
end
-
end
-
end
-
-
1
def render_with_layout(path, locals) #:nodoc:
-
16
layout = path && find_layout(path, locals.keys)
-
16
content = yield(layout)
-
-
16
if layout
-
16
view = @view
-
16
view.view_flow.set(:layout, content)
-
32
layout.render(view, locals){ |*name| view._layout_for(*name) }
-
else
-
content
-
end
-
end
-
-
# This is the method which actually finds the layout using details in the lookup
-
# context object. If no layout is found, it checks if at least a layout with
-
# the given name exists across all details before raising the error.
-
1
def find_layout(layout, keys)
-
32
with_layout_format { resolve_layout(layout, keys) }
-
end
-
-
1
def resolve_layout(layout, keys)
-
32
case layout
-
when String
-
begin
-
if layout =~ /^\//
-
with_fallbacks { find_template(layout, nil, false, keys, @details) }
-
else
-
find_template(layout, nil, false, keys, @details)
-
end
-
rescue ActionView::MissingTemplate
-
all_details = @details.merge(:formats => @lookup_context.default_formats)
-
raise unless template_exists?(layout, nil, false, keys, all_details)
-
end
-
when Proc
-
16
resolve_layout(layout.call, keys)
-
when FalseClass
-
nil
-
else
-
16
layout
-
end
-
end
-
end
-
end
-
2
require "action_view/view_paths"
-
-
2
module ActionView
-
# This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
-
# it will trigger the lookup_context and consequently expire the cache.
-
2
class I18nProxy < ::I18n::Config #:nodoc:
-
2
attr_reader :original_config, :lookup_context
-
-
2
def initialize(original_config, lookup_context)
-
29
original_config = original_config.original_config if original_config.respond_to?(:original_config)
-
29
@original_config, @lookup_context = original_config, lookup_context
-
end
-
-
2
def locale
-
98
@original_config.locale
-
end
-
-
2
def locale=(value)
-
@lookup_context.locale = value
-
end
-
end
-
-
2
module Rendering
-
2
extend ActiveSupport::Concern
-
2
include ActionView::ViewPaths
-
-
# Overwrite process to setup I18n proxy.
-
2
def process(*) #:nodoc:
-
29
old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
-
29
super
-
ensure
-
29
I18n.config = old_config
-
end
-
-
2
module ClassMethods
-
2
def view_context_class
-
@view_context_class ||= begin
-
3
supports_path = supports_path?
-
3
routes = respond_to?(:_routes) && _routes
-
3
helpers = respond_to?(:_helpers) && _helpers
-
-
3
Class.new(ActionView::Base) do
-
3
if routes
-
3
include routes.url_helpers(supports_path)
-
3
include routes.mounted_helpers
-
end
-
-
3
if helpers
-
3
include helpers
-
end
-
end
-
16
end
-
end
-
end
-
-
2
attr_internal_writer :view_context_class
-
-
2
def view_context_class
-
16
@_view_context_class ||= self.class.view_context_class
-
end
-
-
# An instance of a view class. The default view class is ActionView::Base
-
#
-
# The view class must have the following methods:
-
# View.new[lookup_context, assigns, controller]
-
# Create a new ActionView instance for a controller and we can also pass the arguments.
-
# View#render(option)
-
# Returns String with the rendered template
-
#
-
# Override this method in a module to change the default behavior.
-
2
def view_context
-
16
view_context_class.new(view_renderer, view_assigns, self)
-
end
-
-
# Returns an object that is able to render templates.
-
# :api: private
-
2
def view_renderer
-
32
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
-
end
-
-
2
def render_to_body(options = {})
-
16
_process_options(options)
-
16
_render_template(options)
-
end
-
-
2
def rendered_format
-
32
Mime[lookup_context.rendered_format]
-
end
-
-
2
private
-
-
# Find and render a template based on the options given.
-
# :api: private
-
2
def _render_template(options) #:nodoc:
-
16
variant = options[:variant]
-
-
16
lookup_context.rendered_format = nil if options[:formats]
-
16
lookup_context.variants = variant if variant
-
-
16
view_renderer.render(view_context, options)
-
end
-
-
# Assign the rendered format to lookup context.
-
2
def _process_format(format, options = {}) #:nodoc:
-
33
super
-
33
lookup_context.formats = [format.to_sym]
-
33
lookup_context.rendered_format = lookup_context.formats.first
-
end
-
-
# Normalize args by converting render "foo" to render :action => "foo" and
-
# render "foo/bar" to render :template => "foo/bar".
-
# :api: private
-
2
def _normalize_args(action=nil, options={})
-
16
options = super(action, options)
-
16
case action
-
when NilClass
-
when Hash
-
options = action
-
when String, Symbol
-
4
action = action.to_s
-
4
key = action.include?(?/) ? :template : :action
-
4
options[key] = action
-
else
-
options[:partial] = action
-
end
-
-
16
options
-
end
-
-
# Normalize options.
-
# :api: private
-
2
def _normalize_options(options)
-
16
options = super(options)
-
16
if options[:partial] == true
-
options[:partial] = action_name
-
end
-
-
16
if (options.keys & [:partial, :file, :template]).empty?
-
16
options[:prefixes] ||= _prefixes
-
end
-
-
16
options[:template] ||= (options[:action] || action_name).to_s
-
16
options
-
end
-
end
-
end
-
2
require 'action_dispatch/routing/polymorphic_routes'
-
-
2
module ActionView
-
2
module RoutingUrlFor
-
-
# Returns the URL for the set of +options+ provided. This takes the
-
# same options as +url_for+ in Action Controller (see the
-
# documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
-
# <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
-
# instead of the fully qualified URL like "http://example.com/controller/action".
-
#
-
# ==== Options
-
# * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
-
# * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
-
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
-
# is currently not recommended since it breaks caching.
-
# * <tt>:host</tt> - Overrides the default (current) host if provided.
-
# * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
-
# * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
-
# * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
-
#
-
# ==== Relying on named routes
-
#
-
# Passing a record (like an Active Record) instead of a hash as the options parameter will
-
# trigger the named route for that record. The lookup will happen on the name of the class. So passing a
-
# Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
-
# +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
-
#
-
# ==== Implicit Controller Namespacing
-
#
-
# Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one.
-
#
-
# ==== Examples
-
# <%= url_for(action: 'index') %>
-
# # => /blog/
-
#
-
# <%= url_for(action: 'find', controller: 'books') %>
-
# # => /books/find
-
#
-
# <%= url_for(action: 'login', controller: 'members', only_path: false, protocol: 'https') %>
-
# # => https://www.example.com/members/login/
-
#
-
# <%= url_for(action: 'play', anchor: 'player') %>
-
# # => /messages/play/#player
-
#
-
# <%= url_for(action: 'jump', anchor: 'tax&ship') %>
-
# # => /testing/jump/#tax&ship
-
#
-
# <%= url_for(Workshop.new) %>
-
# # relies on Workshop answering a persisted? call (and in this case returning false)
-
# # => /workshops
-
#
-
# <%= url_for(@workshop) %>
-
# # calls @workshop.to_param which by default returns the id
-
# # => /workshops/5
-
#
-
# # to_param can be re-defined in a model to provide different URL names:
-
# # => /workshops/1-workshop-name
-
#
-
# <%= url_for("http://www.example.com") %>
-
# # => http://www.example.com
-
#
-
# <%= url_for(:back) %>
-
# # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
-
# # => http://www.example.com
-
#
-
# <%= url_for(:back) %>
-
# # if request.env["HTTP_REFERER"] is not set or is blank
-
# # => javascript:history.back()
-
#
-
# <%= url_for(action: 'index', controller: 'users') %>
-
# # Assuming an "admin" namespace
-
# # => /admin/users
-
#
-
# <%= url_for(action: 'index', controller: '/users') %>
-
# # Specify absolute path with beginning slash
-
# # => /users
-
2
def url_for(options = nil)
-
158
case options
-
when String
-
135
options
-
when nil
-
super(only_path: _generate_paths_by_default)
-
when Hash
-
options = options.symbolize_keys
-
unless options.key?(:only_path)
-
if options[:host].nil?
-
options[:only_path] = _generate_paths_by_default
-
else
-
options[:only_path] = false
-
end
-
end
-
-
super(options)
-
when :back
-
_back_url
-
when Array
-
components = options.dup
-
if _generate_paths_by_default
-
polymorphic_path(components, components.extract_options!)
-
else
-
polymorphic_url(components, components.extract_options!)
-
end
-
else
-
23
method = _generate_paths_by_default ? :path : :url
-
23
builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method)
-
-
23
case options
-
when Symbol
-
builder.handle_string_call(self, options)
-
when Class
-
builder.handle_class_call(self, options)
-
else
-
23
builder.handle_model_call(self, options)
-
end
-
end
-
end
-
-
2
def url_options #:nodoc:
-
158
return super unless controller.respond_to?(:url_options)
-
158
controller.url_options
-
end
-
-
2
def _routes_context #:nodoc:
-
controller
-
end
-
2
protected :_routes_context
-
-
2
def optimize_routes_generation? #:nodoc:
-
158
controller.respond_to?(:optimize_routes_generation?, true) ?
-
controller.optimize_routes_generation? : super
-
end
-
2
protected :optimize_routes_generation?
-
end
-
end
-
2
require 'action_view/base'
-
-
2
module ActionView
-
2
module ViewPaths
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
4
class_attribute :_view_paths
-
4
self._view_paths = ActionView::PathSet.new
-
4
self._view_paths.freeze
-
end
-
-
2
delegate :template_exists?, :view_paths, :formats, :formats=,
-
:locale, :locale=, :to => :lookup_context
-
-
2
module ClassMethods
-
2
def _prefixes # :nodoc:
-
@_prefixes ||= begin
-
6
deprecated_prefixes = handle_deprecated_parent_prefixes
-
6
if deprecated_prefixes
-
deprecated_prefixes
-
else
-
6
return local_prefixes if superclass.abstract?
-
-
3
local_prefixes + superclass._prefixes
-
end
-
46
end
-
end
-
-
2
private
-
-
# Override this method in your controller if you want to change paths prefixes for finding views.
-
# Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
-
2
def local_prefixes
-
6
[controller_path]
-
end
-
-
2
def handle_deprecated_parent_prefixes # TODO: remove in 4.3/5.0.
-
6
return unless respond_to?(:parent_prefixes)
-
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
Overriding `ActionController::Base::parent_prefixes` is deprecated,
-
override `.local_prefixes` instead.
-
MSG
-
-
local_prefixes + parent_prefixes
-
end
-
end
-
-
# The prefixes used in render "foo" shortcuts.
-
2
def _prefixes # :nodoc:
-
43
self.class._prefixes
-
end
-
-
# LookupContext is the object responsible to hold all information required to lookup
-
# templates, i.e. view paths and details. Check ActionView::LookupContext for more
-
# information.
-
2
def lookup_context
-
@_lookup_context ||=
-
295
ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
-
end
-
-
2
def details_for_lookup
-
27
{ }
-
end
-
-
2
def append_view_path(path)
-
lookup_context.view_paths.push(*path)
-
end
-
-
2
def prepend_view_path(path)
-
lookup_context.view_paths.unshift(*path)
-
end
-
-
2
module ClassMethods
-
# Append a path to the list of view paths for this controller.
-
#
-
# ==== Parameters
-
# * <tt>path</tt> - If a String is provided, it gets converted into
-
# the default view path. You may also provide a custom view path
-
# (see ActionView::PathSet for more information)
-
2
def append_view_path(path)
-
self._view_paths = view_paths + Array(path)
-
end
-
-
# Prepend a path to the list of view paths for this controller.
-
#
-
# ==== Parameters
-
# * <tt>path</tt> - If a String is provided, it gets converted into
-
# the default view path. You may also provide a custom view path
-
# (see ActionView::PathSet for more information)
-
2
def prepend_view_path(path)
-
8
self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
-
end
-
-
# A list of all of the default view paths for this controller.
-
2
def view_paths
-
8
_view_paths
-
end
-
-
# Set the view paths.
-
#
-
# ==== Parameters
-
# * <tt>paths</tt> - If a PathSet is provided, use that;
-
# otherwise, process the parameter into a PathSet.
-
2
def view_paths=(paths)
-
self._view_paths = ActionView::PathSet.new(Array(paths))
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/array/extract_options'
-
-
2
module ActiveModel
-
# == Active \Model \Callbacks
-
#
-
# Provides an interface for any class to have Active Record like callbacks.
-
#
-
# Like the Active Record methods, the callback chain is aborted as soon as
-
# one of the methods in the chain returns +false+.
-
#
-
# First, extend ActiveModel::Callbacks from the class you are creating:
-
#
-
# class MyModel
-
# extend ActiveModel::Callbacks
-
# end
-
#
-
# Then define a list of methods that you want callbacks attached to:
-
#
-
# define_model_callbacks :create, :update
-
#
-
# This will provide all three standard callbacks (before, around and after)
-
# for both the <tt>:create</tt> and <tt>:update</tt> methods. To implement,
-
# you need to wrap the methods you want callbacks on in a block so that the
-
# callbacks get a chance to fire:
-
#
-
# def create
-
# run_callbacks :create do
-
# # Your create action methods here
-
# end
-
# end
-
#
-
# Then in your class, you can use the +before_create+, +after_create+ and
-
# +around_create+ methods, just as you would in an Active Record model.
-
#
-
# before_create :action_before_create
-
#
-
# def action_before_create
-
# # Your code here
-
# end
-
#
-
# When defining an around callback remember to yield to the block, otherwise
-
# it won't be executed:
-
#
-
# around_create :log_status
-
#
-
# def log_status
-
# puts 'going to call the block...'
-
# yield
-
# puts 'block successfully called.'
-
# end
-
#
-
# You can choose not to have all three callbacks by passing a hash to the
-
# +define_model_callbacks+ method.
-
#
-
# define_model_callbacks :create, only: [:after, :before]
-
#
-
# Would only create the +after_create+ and +before_create+ callback methods in
-
# your class.
-
2
module Callbacks
-
2
def self.extended(base) #:nodoc:
-
2
base.class_eval do
-
2
include ActiveSupport::Callbacks
-
end
-
end
-
-
# define_model_callbacks accepts the same options +define_callbacks+ does,
-
# in case you want to overwrite a default. Besides that, it also accepts an
-
# <tt>:only</tt> option, where you can choose if you want all types (before,
-
# around or after) or just some.
-
#
-
# define_model_callbacks :initializer, only: :after
-
#
-
# Note, the <tt>only: <type></tt> hash will apply to all callbacks defined
-
# on that method call. To get around this you can call the define_model_callbacks
-
# method as many times as you need.
-
#
-
# define_model_callbacks :create, only: :after
-
# define_model_callbacks :update, only: :before
-
# define_model_callbacks :destroy, only: :around
-
#
-
# Would create +after_create+, +before_update+ and +around_destroy+ methods
-
# only.
-
#
-
# You can pass in a class to before_<type>, after_<type> and around_<type>,
-
# in which case the callback will call that class's <action>_<type> method
-
# passing the object that the callback is being called on.
-
#
-
# class MyModel
-
# extend ActiveModel::Callbacks
-
# define_model_callbacks :create
-
#
-
# before_create AnotherClass
-
# end
-
#
-
# class AnotherClass
-
# def self.before_create( obj )
-
# # obj is the MyModel instance that the callback is being called on
-
# end
-
# end
-
#
-
# NOTE: +method_name+ passed to `define_model_callbacks` must not end with
-
# `!`, `?` or `=`.
-
2
def define_model_callbacks(*callbacks)
-
4
options = callbacks.extract_options!
-
4
options = {
-
19
terminator: ->(_,result) { result == false },
-
skip_after_callbacks_if_terminated: true,
-
scope: [:kind, :name],
-
only: [:before, :around, :after]
-
}.merge!(options)
-
-
4
types = Array(options.delete(:only))
-
-
4
callbacks.each do |callback|
-
14
define_callbacks(callback, options)
-
-
14
types.each do |type|
-
30
send("_define_#{type}_model_callback", self, callback)
-
end
-
end
-
end
-
-
2
private
-
-
2
def _define_before_model_callback(klass, callback) #:nodoc:
-
8
klass.define_singleton_method("before_#{callback}") do |*args, &block|
-
32
set_callback(:"#{callback}", :before, *args, &block)
-
end
-
end
-
-
2
def _define_around_model_callback(klass, callback) #:nodoc:
-
8
klass.define_singleton_method("around_#{callback}") do |*args, &block|
-
set_callback(:"#{callback}", :around, *args, &block)
-
end
-
end
-
-
2
def _define_after_model_callback(klass, callback) #:nodoc:
-
14
klass.define_singleton_method("after_#{callback}") do |*args, &block|
-
34
options = args.extract_options!
-
34
options[:prepend] = true
-
34
conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
-
8
v != false
-
}
-
34
options[:if] = Array(options[:if]) << conditional
-
34
set_callback(:"#{callback}", :after, *(args << options), &block)
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
# == Active \Model \Conversion
-
#
-
# Handles default conversions: to_model, to_key, to_param, and to_partial_path.
-
#
-
# Let's take for example this non-persisted object.
-
#
-
# class ContactMessage
-
# include ActiveModel::Conversion
-
#
-
# # ContactMessage are never persisted in the DB
-
# def persisted?
-
# false
-
# end
-
# end
-
#
-
# cm = ContactMessage.new
-
# cm.to_model == cm # => true
-
# cm.to_key # => nil
-
# cm.to_param # => nil
-
# cm.to_partial_path # => "contact_messages/contact_message"
-
2
module Conversion
-
2
extend ActiveSupport::Concern
-
-
# If your object is already designed to implement all of the Active Model
-
# you can use the default <tt>:to_model</tt> implementation, which simply
-
# returns +self+.
-
#
-
# class Person
-
# include ActiveModel::Conversion
-
# end
-
#
-
# person = Person.new
-
# person.to_model == person # => true
-
#
-
# If your model does not act like an Active Model object, then you should
-
# define <tt>:to_model</tt> yourself returning a proxy object that wraps
-
# your object with Active Model compliant methods.
-
2
def to_model
-
124
self
-
end
-
-
# Returns an Array of all key attributes if any is set, regardless if
-
# the object is persisted or not. Returns +nil+ if there are no key attributes.
-
#
-
# class Person
-
# include ActiveModel::Conversion
-
# attr_accessor :id
-
# end
-
#
-
# person = Person.create(id: 1)
-
# person.to_key # => [1]
-
2
def to_key
-
key = respond_to?(:id) && id
-
key ? [key] : nil
-
end
-
-
# Returns a +string+ representing the object's key suitable for use in URLs,
-
# or +nil+ if <tt>persisted?</tt> is +false+.
-
#
-
# class Person
-
# include ActiveModel::Conversion
-
# attr_accessor :id
-
# def persisted?
-
# true
-
# end
-
# end
-
#
-
# person = Person.create(id: 1)
-
# person.to_param # => "1"
-
2
def to_param
-
(persisted? && key = to_key) ? key.join('-') : nil
-
end
-
-
# Returns a +string+ identifying the path associated with the object.
-
# ActionPack uses this to find a suitable partial to represent the object.
-
#
-
# class Person
-
# include ActiveModel::Conversion
-
# end
-
#
-
# person = Person.new
-
# person.to_partial_path # => "people/person"
-
2
def to_partial_path
-
self.class._to_partial_path
-
end
-
-
2
module ClassMethods #:nodoc:
-
# Provide a class level cache for #to_partial_path. This is an
-
# internal method and should not be accessed directly.
-
2
def _to_partial_path #:nodoc:
-
@_to_partial_path ||= begin
-
element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
-
collection = ActiveSupport::Inflector.tableize(name)
-
"#{collection}/#{element}".freeze
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/hash_with_indifferent_access'
-
2
require 'active_support/core_ext/object/duplicable'
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveModel
-
# == Active \Model \Dirty
-
#
-
# Provides a way to track changes in your object in the same way as
-
# Active Record does.
-
#
-
# The requirements for implementing ActiveModel::Dirty are:
-
#
-
# * <tt>include ActiveModel::Dirty</tt> in your object.
-
# * Call <tt>define_attribute_methods</tt> passing each method you want to
-
# track.
-
# * Call <tt>attr_name_will_change!</tt> before each change to the tracked
-
# attribute.
-
# * Call <tt>changes_applied</tt> after the changes are persisted.
-
# * Call <tt>clear_changes_information</tt> when you want to reset the changes
-
# information.
-
# * Call <tt>restore_attributes</tt> when you want to restore previous data.
-
#
-
# A minimal implementation could be:
-
#
-
# class Person
-
# include ActiveModel::Dirty
-
#
-
# define_attribute_methods :name
-
#
-
# def initialize(name)
-
# @name = name
-
# end
-
#
-
# def name
-
# @name
-
# end
-
#
-
# def name=(val)
-
# name_will_change! unless val == @name
-
# @name = val
-
# end
-
#
-
# def save
-
# # do persistence work
-
#
-
# changes_applied
-
# end
-
#
-
# def reload!
-
# # get the values from the persistence layer
-
#
-
# clear_changes_information
-
# end
-
#
-
# def rollback!
-
# restore_attributes
-
# end
-
# end
-
#
-
# A newly instantiated +Person+ object is unchanged:
-
#
-
# person = Person.new("Uncle Bob")
-
# person.changed? # => false
-
#
-
# Change the name:
-
#
-
# person.name = 'Bob'
-
# person.changed? # => true
-
# person.name_changed? # => true
-
# person.name_changed?(from: "Uncle Bob", to: "Bob") # => true
-
# person.name_was # => "Uncle Bob"
-
# person.name_change # => ["Uncle Bob", "Bob"]
-
# person.name = 'Bill'
-
# person.name_change # => ["Uncle Bob", "Bill"]
-
#
-
# Save the changes:
-
#
-
# person.save
-
# person.changed? # => false
-
# person.name_changed? # => false
-
#
-
# Reset the changes:
-
#
-
# person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
-
# person.reload!
-
# person.previous_changes # => {}
-
#
-
# Rollback the changes:
-
#
-
# person.name = "Uncle Bob"
-
# person.rollback!
-
# person.name # => "Bill"
-
# person.name_changed? # => false
-
#
-
# Assigning the same value leaves the attribute unchanged:
-
#
-
# person.name = 'Bill'
-
# person.name_changed? # => false
-
# person.name_change # => nil
-
#
-
# Which attributes have changed?
-
#
-
# person.name = 'Bob'
-
# person.changed # => ["name"]
-
# person.changes # => {"name" => ["Bill", "Bob"]}
-
#
-
# If an attribute is modified in-place then make use of
-
# +[attribute_name]_will_change!+ to mark that the attribute is changing.
-
# Otherwise Active Model can't track changes to in-place attributes. Note
-
# that Active Record can detect in-place modifications automatically. You do
-
# not need to call +[attribute_name]_will_change!+ on Active Record models.
-
#
-
# person.name_will_change!
-
# person.name_change # => ["Bill", "Bill"]
-
# person.name << 'y'
-
# person.name_change # => ["Bill", "Billy"]
-
2
module Dirty
-
2
extend ActiveSupport::Concern
-
2
include ActiveModel::AttributeMethods
-
-
2
included do
-
2
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
-
2
attribute_method_affix prefix: 'reset_', suffix: '!'
-
2
attribute_method_affix prefix: 'restore_', suffix: '!'
-
end
-
-
# Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
-
#
-
# person.changed? # => false
-
# person.name = 'bob'
-
# person.changed? # => true
-
2
def changed?
-
4
changed_attributes.present?
-
end
-
-
# Returns an array with the name of the attributes with unsaved changes.
-
#
-
# person.changed # => []
-
# person.name = 'bob'
-
# person.changed # => ["name"]
-
2
def changed
-
20
changed_attributes.keys
-
end
-
-
# Returns a hash of changed attributes indicating their original
-
# and new values like <tt>attr => [original value, new value]</tt>.
-
#
-
# person.changes # => {}
-
# person.name = 'bob'
-
# person.changes # => { "name" => ["bill", "bob"] }
-
2
def changes
-
43
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
-
end
-
-
# Returns a hash of attributes that were changed before the model was saved.
-
#
-
# person.name # => "bob"
-
# person.name = 'robert'
-
# person.save
-
# person.previous_changes # => {"name" => ["bob", "robert"]}
-
2
def previous_changes
-
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
-
end
-
-
# Returns a hash of the attributes with unsaved changes indicating their original
-
# values like <tt>attr => original value</tt>.
-
#
-
# person.name # => "bob"
-
# person.name = 'robert'
-
# person.changed_attributes # => {"name" => "bob"}
-
2
def changed_attributes
-
258
@changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
-
end
-
-
# Handle <tt>*_changed?</tt> for +method_missing+.
-
2
def attribute_changed?(attr, options = {}) #:nodoc:
-
109
result = changes_include?(attr)
-
109
result &&= options[:to] == __send__(attr) if options.key?(:to)
-
109
result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
-
109
result
-
end
-
-
# Handle <tt>*_was</tt> for +method_missing+.
-
2
def attribute_was(attr) # :nodoc:
-
2
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
-
end
-
-
# Restore all previous data of the provided attributes.
-
2
def restore_attributes(attributes = changed)
-
attributes.each { |attr| restore_attribute! attr }
-
end
-
-
2
private
-
-
2
def changes_include?(attr_name)
-
178
attributes_changed_by_setter.include?(attr_name)
-
end
-
2
alias attribute_changed_by_setter? changes_include?
-
-
# Removes current changes and makes them accessible through +previous_changes+.
-
2
def changes_applied # :doc:
-
10
@previously_changed = changes
-
10
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
-
end
-
-
# Clear all dirty data: current changes and previous changes.
-
2
def clear_changes_information # :doc:
-
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
-
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
-
end
-
-
2
def reset_changes
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
`#reset_changes` is deprecated and will be removed on Rails 5.
-
Please use `#clear_changes_information` instead.
-
MSG
-
-
clear_changes_information
-
end
-
-
# Handle <tt>*_change</tt> for +method_missing+.
-
2
def attribute_change(attr)
-
33
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
-
end
-
-
# Handle <tt>*_will_change!</tt> for +method_missing+.
-
2
def attribute_will_change!(attr)
-
return if attribute_changed?(attr)
-
-
begin
-
value = __send__(attr)
-
value = value.duplicable? ? value.clone : value
-
rescue TypeError, NoMethodError
-
end
-
-
set_attribute_was(attr, value)
-
end
-
-
# Handle <tt>reset_*!</tt> for +method_missing+.
-
2
def reset_attribute!(attr)
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
`#reset_#{attr}!` is deprecated and will be removed on Rails 5.
-
Please use `#restore_#{attr}!` instead.
-
MSG
-
-
restore_attribute!(attr)
-
end
-
-
# Handle <tt>restore_*!</tt> for +method_missing+.
-
2
def restore_attribute!(attr)
-
if attribute_changed?(attr)
-
__send__("#{attr}=", changed_attributes[attr])
-
clear_attribute_changes([attr])
-
end
-
end
-
-
# This is necessary because `changed_attributes` might be overridden in
-
# other implemntations (e.g. in `ActiveRecord`)
-
2
alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
-
-
# Force an attribute to have a particular "before" value
-
2
def set_attribute_was(attr, old_value)
-
55
attributes_changed_by_setter[attr] = old_value
-
end
-
-
# Remove changes information for the provided attributes.
-
2
def clear_attribute_changes(attributes) # :doc:
-
attributes_changed_by_setter.except!(*attributes)
-
end
-
end
-
end
-
# -*- coding: utf-8 -*-
-
-
1
require 'active_support/core_ext/array/conversions'
-
1
require 'active_support/core_ext/string/inflections'
-
-
1
module ActiveModel
-
# == Active \Model \Errors
-
#
-
# Provides a modified +Hash+ that you can include in your object
-
# for handling error messages and interacting with Action View helpers.
-
#
-
# A minimal implementation could be:
-
#
-
# class Person
-
# # Required dependency for ActiveModel::Errors
-
# extend ActiveModel::Naming
-
#
-
# def initialize
-
# @errors = ActiveModel::Errors.new(self)
-
# end
-
#
-
# attr_accessor :name
-
# attr_reader :errors
-
#
-
# def validate!
-
# errors.add(:name, "cannot be nil") if name.nil?
-
# end
-
#
-
# # The following methods are needed to be minimally implemented
-
#
-
# def read_attribute_for_validation(attr)
-
# send(attr)
-
# end
-
#
-
# def Person.human_attribute_name(attr, options = {})
-
# attr
-
# end
-
#
-
# def Person.lookup_ancestors
-
# [self]
-
# end
-
# end
-
#
-
# The last three methods are required in your object for Errors to be
-
# able to generate error messages correctly and also handle multiple
-
# languages. Of course, if you extend your object with ActiveModel::Translation
-
# you will not need to implement the last two. Likewise, using
-
# ActiveModel::Validations will handle the validation related methods
-
# for you.
-
#
-
# The above allows you to do:
-
#
-
# person = Person.new
-
# person.validate! # => ["cannot be nil"]
-
# person.errors.full_messages # => ["name cannot be nil"]
-
# # etc..
-
1
class Errors
-
1
include Enumerable
-
-
1
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
-
-
1
attr_reader :messages
-
-
# Pass in the instance of the object that is using the errors object.
-
#
-
# class Person
-
# def initialize
-
# @errors = ActiveModel::Errors.new(self)
-
# end
-
# end
-
1
def initialize(base)
-
26
@base = base
-
26
@messages = {}
-
end
-
-
1
def initialize_dup(other) # :nodoc:
-
@messages = other.messages.dup
-
super
-
end
-
-
# Clear the error messages.
-
#
-
# person.errors.full_messages # => ["name cannot be nil"]
-
# person.errors.clear
-
# person.errors.full_messages # => []
-
1
def clear
-
21
messages.clear
-
end
-
-
# Returns +true+ if the error messages include an error for the given key
-
# +attribute+, +false+ otherwise.
-
#
-
# person.errors.messages # => {:name=>["cannot be nil"]}
-
# person.errors.include?(:name) # => true
-
# person.errors.include?(:age) # => false
-
1
def include?(attribute)
-
messages[attribute].present?
-
end
-
# aliases include?
-
1
alias :has_key? :include?
-
# aliases include?
-
1
alias :key? :include?
-
-
# Get messages for +key+.
-
#
-
# person.errors.messages # => {:name=>["cannot be nil"]}
-
# person.errors.get(:name) # => ["cannot be nil"]
-
# person.errors.get(:age) # => nil
-
1
def get(key)
-
115
messages[key]
-
end
-
-
# Set messages for +key+ to +value+.
-
#
-
# person.errors.get(:name) # => ["cannot be nil"]
-
# person.errors.set(:name, ["can't be nil"])
-
# person.errors.get(:name) # => ["can't be nil"]
-
1
def set(key, value)
-
48
messages[key] = value
-
end
-
-
# Delete messages for +key+. Returns the deleted messages.
-
#
-
# person.errors.get(:name) # => ["cannot be nil"]
-
# person.errors.delete(:name) # => ["cannot be nil"]
-
# person.errors.get(:name) # => nil
-
1
def delete(key)
-
messages.delete(key)
-
end
-
-
# When passed a symbol or a name of a method, returns an array of errors
-
# for the method.
-
#
-
# person.errors[:name] # => ["cannot be nil"]
-
# person.errors['name'] # => ["cannot be nil"]
-
1
def [](attribute)
-
115
get(attribute.to_sym) || set(attribute.to_sym, [])
-
end
-
-
# Adds to the supplied attribute the supplied error message.
-
#
-
# person.errors[:name] = "must be set"
-
# person.errors[:name] # => ['must be set']
-
1
def []=(attribute, error)
-
self[attribute] << error
-
end
-
-
# Iterates through each error key, value pair in the error messages hash.
-
# Yields the attribute and the error for that attribute. If the attribute
-
# has more than one error message, yields once for each error message.
-
#
-
# person.errors.add(:name, "can't be blank")
-
# person.errors.each do |attribute, error|
-
# # Will yield :name and "can't be blank"
-
# end
-
#
-
# person.errors.add(:name, "must be specified")
-
# person.errors.each do |attribute, error|
-
# # Will yield :name and "can't be blank"
-
# # then yield :name and "must be specified"
-
# end
-
1
def each
-
60
messages.each_key do |attribute|
-
64
self[attribute].each { |error| yield attribute, error }
-
end
-
end
-
-
# Returns the number of error messages.
-
#
-
# person.errors.add(:name, "can't be blank")
-
# person.errors.size # => 1
-
# person.errors.add(:name, "must be specified")
-
# person.errors.size # => 2
-
1
def size
-
values.flatten.size
-
end
-
-
# Returns all message values.
-
#
-
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
-
# person.errors.values # => [["cannot be nil", "must be specified"]]
-
1
def values
-
messages.values
-
end
-
-
# Returns all message keys.
-
#
-
# person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
-
# person.errors.keys # => [:name]
-
1
def keys
-
messages.keys
-
end
-
-
# Returns an array of error messages, with the attribute name included.
-
#
-
# person.errors.add(:name, "can't be blank")
-
# person.errors.add(:name, "must be specified")
-
# person.errors.to_a # => ["name can't be blank", "name must be specified"]
-
1
def to_a
-
4
full_messages
-
end
-
-
# Returns the number of error messages.
-
#
-
# person.errors.add(:name, "can't be blank")
-
# person.errors.count # => 1
-
# person.errors.add(:name, "must be specified")
-
# person.errors.count # => 2
-
1
def count
-
4
to_a.size
-
end
-
-
# Returns +true+ if no errors are found, +false+ otherwise.
-
# If the error message is a string it can be empty.
-
#
-
# person.errors.full_messages # => ["name cannot be nil"]
-
# person.errors.empty? # => false
-
1
def empty?
-
62
all? { |k, v| v && v.empty? && !v.is_a?(String) }
-
end
-
# aliases empty?
-
1
alias_method :blank?, :empty?
-
-
# Returns an xml formatted representation of the Errors hash.
-
#
-
# person.errors.add(:name, "can't be blank")
-
# person.errors.add(:name, "must be specified")
-
# person.errors.to_xml
-
# # =>
-
# # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
-
# # <errors>
-
# # <error>name can't be blank</error>
-
# # <error>name must be specified</error>
-
# # </errors>
-
1
def to_xml(options={})
-
to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
-
end
-
-
# Returns a Hash that can be used as the JSON representation for this
-
# object. You can pass the <tt>:full_messages</tt> option. This determines
-
# if the json object should contain full messages or not (false by default).
-
#
-
# person.errors.as_json # => {:name=>["cannot be nil"]}
-
# person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
-
1
def as_json(options=nil)
-
to_hash(options && options[:full_messages])
-
end
-
-
# Returns a Hash of attributes with their error messages. If +full_messages+
-
# is +true+, it will contain full messages (see +full_message+).
-
#
-
# person.errors.to_hash # => {:name=>["cannot be nil"]}
-
# person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
-
1
def to_hash(full_messages = false)
-
if full_messages
-
self.messages.each_with_object({}) do |(attribute, array), messages|
-
messages[attribute] = array.map { |message| full_message(attribute, message) }
-
end
-
else
-
self.messages.dup
-
end
-
end
-
-
# Adds +message+ to the error messages on +attribute+. More than one error
-
# can be added to the same +attribute+. If no +message+ is supplied,
-
# <tt>:invalid</tt> is assumed.
-
#
-
# person.errors.add(:name)
-
# # => ["is invalid"]
-
# person.errors.add(:name, 'must be implemented')
-
# # => ["is invalid", "must be implemented"]
-
#
-
# person.errors.messages
-
# # => {:name=>["must be implemented", "is invalid"]}
-
#
-
# If +message+ is a symbol, it will be translated using the appropriate
-
# scope (see +generate_message+).
-
#
-
# If +message+ is a proc, it will be called, allowing for things like
-
# <tt>Time.now</tt> to be used within an error.
-
#
-
# If the <tt>:strict</tt> option is set to +true+, it will raise
-
# ActiveModel::StrictValidationFailed instead of adding the error.
-
# <tt>:strict</tt> option can also be set to any other exception.
-
#
-
# person.errors.add(:name, nil, strict: true)
-
# # => ActiveModel::StrictValidationFailed: name is invalid
-
# person.errors.add(:name, nil, strict: NameIsInvalid)
-
# # => NameIsInvalid: name is invalid
-
#
-
# person.errors.messages # => {}
-
#
-
# +attribute+ should be set to <tt>:base</tt> if the error is not
-
# directly associated with a single attribute.
-
#
-
# person.errors.add(:base, "either name or email must be present")
-
# person.errors.messages
-
# # => {:base=>["either name or email must be present"]}
-
1
def add(attribute, message = :invalid, options = {})
-
17
message = normalize_message(attribute, message, options)
-
17
if exception = options[:strict]
-
exception = ActiveModel::StrictValidationFailed if exception == true
-
raise exception, full_message(attribute, message)
-
end
-
-
17
self[attribute] << message
-
end
-
-
# Will add an error message to each of the attributes in +attributes+
-
# that is empty.
-
#
-
# person.errors.add_on_empty(:name)
-
# person.errors.messages
-
# # => {:name=>["can't be empty"]}
-
1
def add_on_empty(attributes, options = {})
-
Array(attributes).each do |attribute|
-
value = @base.send(:read_attribute_for_validation, attribute)
-
is_empty = value.respond_to?(:empty?) ? value.empty? : false
-
add(attribute, :empty, options) if value.nil? || is_empty
-
end
-
end
-
-
# Will add an error message to each of the attributes in +attributes+ that
-
# is blank (using Object#blank?).
-
#
-
# person.errors.add_on_blank(:name)
-
# person.errors.messages
-
# # => {:name=>["can't be blank"]}
-
1
def add_on_blank(attributes, options = {})
-
Array(attributes).each do |attribute|
-
value = @base.send(:read_attribute_for_validation, attribute)
-
add(attribute, :blank, options) if value.blank?
-
end
-
end
-
-
# Returns +true+ if an error on the attribute with the given message is
-
# present, +false+ otherwise. +message+ is treated the same as for +add+.
-
#
-
# person.errors.add :name, :blank
-
# person.errors.added? :name, :blank # => true
-
1
def added?(attribute, message = :invalid, options = {})
-
message = normalize_message(attribute, message, options)
-
self[attribute].include? message
-
end
-
-
# Returns all the full error messages in an array.
-
#
-
# class Person
-
# validates_presence_of :name, :address, :email
-
# validates_length_of :name, in: 5..30
-
# end
-
#
-
# person = Person.create(address: '123 First St.')
-
# person.errors.full_messages
-
# # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
-
1
def full_messages
-
16
map { |attribute, message| full_message(attribute, message) }
-
end
-
-
# Returns all the full error messages for a given attribute in an array.
-
#
-
# class Person
-
# validates_presence_of :name, :email
-
# validates_length_of :name, in: 5..30
-
# end
-
#
-
# person = Person.create()
-
# person.errors.full_messages_for(:name)
-
# # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
-
1
def full_messages_for(attribute)
-
(get(attribute) || []).map { |message| full_message(attribute, message) }
-
end
-
-
# Returns a full message for a given attribute.
-
#
-
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
-
1
def full_message(attribute, message)
-
8
return message if attribute == :base
-
8
attr_name = attribute.to_s.tr('.', '_').humanize
-
8
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
-
8
I18n.t(:"errors.format", {
-
default: "%{attribute} %{message}",
-
attribute: attr_name,
-
message: message
-
})
-
end
-
-
# Translates an error message in its default scope
-
# (<tt>activemodel.errors.messages</tt>).
-
#
-
# Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
-
# if it's not there, it's looked up in <tt>models.MODEL.MESSAGE</tt> and if
-
# that is not there also, it returns the translation of the default message
-
# (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
-
# name, translated attribute name and the value are available for
-
# interpolation.
-
#
-
# When using inheritance in your models, it will check all the inherited
-
# models too, but only if the model itself hasn't been found. Say you have
-
# <tt>class Admin < User; end</tt> and you wanted the translation for
-
# the <tt>:blank</tt> error message for the <tt>title</tt> attribute,
-
# it looks for these translations:
-
#
-
# * <tt>activemodel.errors.models.admin.attributes.title.blank</tt>
-
# * <tt>activemodel.errors.models.admin.blank</tt>
-
# * <tt>activemodel.errors.models.user.attributes.title.blank</tt>
-
# * <tt>activemodel.errors.models.user.blank</tt>
-
# * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope)
-
# * <tt>activemodel.errors.messages.blank</tt>
-
# * <tt>errors.attributes.title.blank</tt>
-
# * <tt>errors.messages.blank</tt>
-
1
def generate_message(attribute, type = :invalid, options = {})
-
17
type = options.delete(:message) if options[:message].is_a?(Symbol)
-
-
17
if @base.class.respond_to?(:i18n_scope)
-
17
defaults = @base.class.lookup_ancestors.map do |klass|
-
17
[ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
-
:"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
-
end
-
else
-
defaults = []
-
end
-
-
17
defaults << options.delete(:message)
-
17
defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope)
-
17
defaults << :"errors.attributes.#{attribute}.#{type}"
-
17
defaults << :"errors.messages.#{type}"
-
-
17
defaults.compact!
-
17
defaults.flatten!
-
-
17
key = defaults.shift
-
17
value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
-
-
17
options = {
-
default: defaults,
-
model: @base.model_name.human,
-
attribute: @base.class.human_attribute_name(attribute),
-
value: value
-
}.merge!(options)
-
-
17
I18n.translate(key, options)
-
end
-
-
1
private
-
1
def normalize_message(attribute, message, options)
-
17
case message
-
when Symbol
-
17
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
-
when Proc
-
message.call
-
else
-
message
-
end
-
end
-
end
-
-
# Raised when a validation cannot be corrected by end users and are considered
-
# exceptional.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
#
-
# validates_presence_of :name, strict: true
-
# end
-
#
-
# person = Person.new
-
# person.name = nil
-
# person.valid?
-
# # => ActiveModel::StrictValidationFailed: Name can't be blank
-
1
class StrictValidationFailed < StandardError
-
end
-
end
-
2
module ActiveModel
-
# Raised when forbidden attributes are used for mass assignment.
-
#
-
# class Person < ActiveRecord::Base
-
# end
-
#
-
# params = ActionController::Parameters.new(name: 'Bob')
-
# Person.new(params)
-
# # => ActiveModel::ForbiddenAttributesError
-
#
-
# params.permit!
-
# Person.new(params)
-
# # => #<Person id: nil, name: "Bob">
-
2
class ForbiddenAttributesError < StandardError
-
end
-
-
2
module ForbiddenAttributesProtection # :nodoc:
-
2
protected
-
2
def sanitize_for_mass_assignment(attributes)
-
119
if attributes.respond_to?(:permitted?) && !attributes.permitted?
-
raise ActiveModel::ForbiddenAttributesError
-
else
-
119
attributes
-
end
-
end
-
2
alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
-
end
-
end
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/module/introspection'
-
2
require 'active_support/core_ext/module/remove_method'
-
-
2
module ActiveModel
-
2
class Name
-
2
include Comparable
-
-
2
attr_reader :singular, :plural, :element, :collection,
-
:singular_route_key, :route_key, :param_key, :i18n_key,
-
:name
-
-
2
alias_method :cache_key, :collection
-
-
##
-
# :method: ==
-
#
-
# :call-seq:
-
# ==(other)
-
#
-
# Equivalent to <tt>String#==</tt>. Returns +true+ if the class name and
-
# +other+ are equal, otherwise +false+.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name == 'BlogPost' # => true
-
# BlogPost.model_name == 'Blog Post' # => false
-
-
##
-
# :method: ===
-
#
-
# :call-seq:
-
# ===(other)
-
#
-
# Equivalent to <tt>#==</tt>.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name === 'BlogPost' # => true
-
# BlogPost.model_name === 'Blog Post' # => false
-
-
##
-
# :method: <=>
-
#
-
# :call-seq:
-
# ==(other)
-
#
-
# Equivalent to <tt>String#<=></tt>.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name <=> 'BlogPost' # => 0
-
# BlogPost.model_name <=> 'Blog' # => 1
-
# BlogPost.model_name <=> 'BlogPosts' # => -1
-
-
##
-
# :method: =~
-
#
-
# :call-seq:
-
# =~(regexp)
-
#
-
# Equivalent to <tt>String#=~</tt>. Match the class name against the given
-
# regexp. Returns the position where the match starts or +nil+ if there is
-
# no match.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name =~ /Post/ # => 4
-
# BlogPost.model_name =~ /\d/ # => nil
-
-
##
-
# :method: !~
-
#
-
# :call-seq:
-
# !~(regexp)
-
#
-
# Equivalent to <tt>String#!~</tt>. Match the class name against the given
-
# regexp. Returns +true+ if there is no match, otherwise +false+.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name !~ /Post/ # => false
-
# BlogPost.model_name !~ /\d/ # => true
-
-
##
-
# :method: eql?
-
#
-
# :call-seq:
-
# eql?(other)
-
#
-
# Equivalent to <tt>String#eql?</tt>. Returns +true+ if the class name and
-
# +other+ have the same length and content, otherwise +false+.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name.eql?('BlogPost') # => true
-
# BlogPost.model_name.eql?('Blog Post') # => false
-
-
##
-
# :method: to_s
-
#
-
# :call-seq:
-
# to_s()
-
#
-
# Returns the class name.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name.to_s # => "BlogPost"
-
-
##
-
# :method: to_str
-
#
-
# :call-seq:
-
# to_str()
-
#
-
# Equivalent to +to_s+.
-
2
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
-
:to_str, :as_json, to: :name
-
-
# Returns a new ActiveModel::Name instance. By default, the +namespace+
-
# and +name+ option will take the namespace and name of the given class
-
# respectively.
-
#
-
# module Foo
-
# class Bar
-
# end
-
# end
-
#
-
# ActiveModel::Name.new(Foo::Bar).to_s
-
# # => "Foo::Bar"
-
2
def initialize(klass, namespace = nil, name = nil)
-
3
@name = name || klass.name
-
-
3
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
-
-
3
@unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace
-
3
@klass = klass
-
3
@singular = _singularize(@name)
-
3
@plural = ActiveSupport::Inflector.pluralize(@singular)
-
3
@element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
-
3
@human = ActiveSupport::Inflector.humanize(@element)
-
3
@collection = ActiveSupport::Inflector.tableize(@name)
-
3
@param_key = (namespace ? _singularize(@unnamespaced) : @singular)
-
3
@i18n_key = @name.underscore.to_sym
-
-
3
@route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
-
3
@singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
-
3
@route_key << "_index" if @plural == @singular
-
end
-
-
# Transform the model name into a more humane format, using I18n. By default,
-
# it will underscore then humanize the class name.
-
#
-
# class BlogPost
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BlogPost.model_name.human # => "Blog post"
-
#
-
# Specify +options+ with additional translating options.
-
2
def human(options={})
-
return @human unless @klass.respond_to?(:lookup_ancestors) &&
-
19
@klass.respond_to?(:i18n_scope)
-
-
19
defaults = @klass.lookup_ancestors.map do |klass|
-
19
klass.model_name.i18n_key
-
end
-
-
19
defaults << options[:default] if options[:default]
-
19
defaults << @human
-
-
19
options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
-
19
I18n.translate(defaults.shift, options)
-
end
-
-
2
private
-
-
2
def _singularize(string, replacement='_')
-
3
ActiveSupport::Inflector.underscore(string).tr('/', replacement)
-
end
-
end
-
-
# == Active \Model \Naming
-
#
-
# Creates a +model_name+ method on your object.
-
#
-
# To implement, just extend ActiveModel::Naming in your object:
-
#
-
# class BookCover
-
# extend ActiveModel::Naming
-
# end
-
#
-
# BookCover.model_name.name # => "BookCover"
-
# BookCover.model_name.human # => "Book cover"
-
#
-
# BookCover.model_name.i18n_key # => :book_cover
-
# BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
-
#
-
# Providing the functionality that ActiveModel::Naming provides in your object
-
# is required to pass the Active Model Lint test. So either extending the
-
# provided method below, or rolling your own is required.
-
2
module Naming
-
2
def self.extended(base) #:nodoc:
-
8
base.remove_possible_method :model_name
-
8
base.delegate :model_name, to: :class
-
end
-
-
# Returns an ActiveModel::Name object for module. It can be
-
# used to retrieve all kinds of naming-related information
-
# (See ActiveModel::Name for more information).
-
#
-
# class Person
-
# extend ActiveModel::Naming
-
# end
-
#
-
# Person.model_name.name # => "Person"
-
# Person.model_name.class # => ActiveModel::Name
-
# Person.model_name.singular # => "person"
-
# Person.model_name.plural # => "people"
-
2
def model_name
-
@_model_name ||= begin
-
3
namespace = self.parents.detect do |n|
-
3
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
-
end
-
3
ActiveModel::Name.new(self, namespace)
-
232
end
-
end
-
-
# Returns the plural class name of a record or class.
-
#
-
# ActiveModel::Naming.plural(post) # => "posts"
-
# ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people"
-
2
def self.plural(record_or_class)
-
model_name_from_record_or_class(record_or_class).plural
-
end
-
-
# Returns the singular class name of a record or class.
-
#
-
# ActiveModel::Naming.singular(post) # => "post"
-
# ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person"
-
2
def self.singular(record_or_class)
-
model_name_from_record_or_class(record_or_class).singular
-
end
-
-
# Identifies whether the class name of a record or class is uncountable.
-
#
-
# ActiveModel::Naming.uncountable?(Sheep) # => true
-
# ActiveModel::Naming.uncountable?(Post) # => false
-
2
def self.uncountable?(record_or_class)
-
plural(record_or_class) == singular(record_or_class)
-
end
-
-
# Returns string to use while generating route names. It differs for
-
# namespaced models regarding whether it's inside isolated engine.
-
#
-
# # For isolated engine:
-
# ActiveModel::Naming.singular_route_key(Blog::Post) # => "post"
-
#
-
# # For shared engine:
-
# ActiveModel::Naming.singular_route_key(Blog::Post) # => "blog_post"
-
2
def self.singular_route_key(record_or_class)
-
model_name_from_record_or_class(record_or_class).singular_route_key
-
end
-
-
# Returns string to use while generating route names. It differs for
-
# namespaced models regarding whether it's inside isolated engine.
-
#
-
# # For isolated engine:
-
# ActiveModel::Naming.route_key(Blog::Post) # => "posts"
-
#
-
# # For shared engine:
-
# ActiveModel::Naming.route_key(Blog::Post) # => "blog_posts"
-
#
-
# The route key also considers if the noun is uncountable and, in
-
# such cases, automatically appends _index.
-
2
def self.route_key(record_or_class)
-
model_name_from_record_or_class(record_or_class).route_key
-
end
-
-
# Returns string to use for params names. It differs for
-
# namespaced models regarding whether it's inside isolated engine.
-
#
-
# # For isolated engine:
-
# ActiveModel::Naming.param_key(Blog::Post) # => "post"
-
#
-
# # For shared engine:
-
# ActiveModel::Naming.param_key(Blog::Post) # => "blog_post"
-
2
def self.param_key(record_or_class)
-
model_name_from_record_or_class(record_or_class).param_key
-
end
-
-
2
def self.model_name_from_record_or_class(record_or_class) #:nodoc:
-
if record_or_class.respond_to?(:to_model)
-
record_or_class.to_model.model_name
-
else
-
record_or_class.model_name
-
end
-
end
-
2
private_class_method :model_name_from_record_or_class
-
end
-
end
-
2
module ActiveModel
-
2
module SecurePassword
-
2
extend ActiveSupport::Concern
-
-
# BCrypt hash function can handle maximum 72 characters, and if we pass
-
# password of length more than 72 characters it ignores extra characters.
-
# Hence need to put a restriction on password length.
-
2
MAX_PASSWORD_LENGTH_ALLOWED = 72
-
-
2
class << self
-
2
attr_accessor :min_cost # :nodoc:
-
end
-
2
self.min_cost = false
-
-
2
module ClassMethods
-
# Adds methods to set and authenticate against a BCrypt password.
-
# This mechanism requires you to have a +password_digest+ attribute.
-
#
-
# The following validations are added automatically:
-
# * Password must be present on creation
-
# * Password length should be less than or equal to 72 characters
-
# * Confirmation of password (using a +password_confirmation+ attribute)
-
#
-
# If password confirmation validation is not needed, simply leave out the
-
# value for +password_confirmation+ (i.e. don't provide a form field for
-
# it). When this attribute has a +nil+ value, the validation will not be
-
# triggered.
-
#
-
# For further customizability, it is possible to supress the default
-
# validations by passing <tt>validations: false</tt> as an argument.
-
#
-
# Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
-
#
-
# gem 'bcrypt', '~> 3.1.7'
-
#
-
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
-
#
-
# # Schema: User(name:string, password_digest:string)
-
# class User < ActiveRecord::Base
-
# has_secure_password
-
# end
-
#
-
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
-
# user.save # => false, password required
-
# user.password = 'mUc3m00RsqyRe'
-
# user.save # => false, confirmation doesn't match
-
# user.password_confirmation = 'mUc3m00RsqyRe'
-
# user.save # => true
-
# user.authenticate('notright') # => false
-
# user.authenticate('mUc3m00RsqyRe') # => user
-
# User.find_by(name: 'david').try(:authenticate, 'notright') # => false
-
# User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
-
2
def has_secure_password(options = {})
-
# Load bcrypt gem only when has_secure_password is used.
-
# This is to avoid ActiveModel (and by extension the entire framework)
-
# being dependent on a binary library.
-
begin
-
require 'bcrypt'
-
rescue LoadError
-
$stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
-
raise
-
end
-
-
include InstanceMethodsOnActivation
-
-
if options.fetch(:validations, true)
-
include ActiveModel::Validations
-
-
# This ensures the model has a password by checking whether the password_digest
-
# is present, so that this works with both new and existing records. However,
-
# when there is an error, the message is added to the password attribute instead
-
# so that the error message will make sense to the end-user.
-
validate do |record|
-
record.errors.add(:password, :blank) unless record.password_digest.present?
-
end
-
-
validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
-
validates_confirmation_of :password, allow_blank: true
-
end
-
-
# This code is necessary as long as the protected_attributes gem is supported.
-
if respond_to?(:attributes_protected_by_default)
-
def self.attributes_protected_by_default #:nodoc:
-
super + ['password_digest']
-
end
-
end
-
end
-
end
-
-
2
module InstanceMethodsOnActivation
-
# Returns +self+ if the password is correct, otherwise +false+.
-
#
-
# class User < ActiveRecord::Base
-
# has_secure_password validations: false
-
# end
-
#
-
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
-
# user.save
-
# user.authenticate('notright') # => false
-
# user.authenticate('mUc3m00RsqyRe') # => user
-
2
def authenticate(unencrypted_password)
-
BCrypt::Password.new(password_digest) == unencrypted_password && self
-
end
-
-
2
attr_reader :password
-
-
# Encrypts the password into the +password_digest+ attribute, only if the
-
# new password is not empty.
-
#
-
# class User < ActiveRecord::Base
-
# has_secure_password validations: false
-
# end
-
#
-
# user = User.new
-
# user.password = nil
-
# user.password_digest # => nil
-
# user.password = 'mUc3m00RsqyRe'
-
# user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
-
2
def password=(unencrypted_password)
-
if unencrypted_password.nil?
-
self.password_digest = nil
-
elsif !unencrypted_password.empty?
-
@password = unencrypted_password
-
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
-
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
-
end
-
end
-
-
2
def password_confirmation=(unencrypted_password)
-
@password_confirmation = unencrypted_password
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/hash/slice'
-
-
2
module ActiveModel
-
# == Active \Model \Serialization
-
#
-
# Provides a basic serialization to a serializable_hash for your objects.
-
#
-
# A minimal implementation could be:
-
#
-
# class Person
-
# include ActiveModel::Serialization
-
#
-
# attr_accessor :name
-
#
-
# def attributes
-
# {'name' => nil}
-
# end
-
# end
-
#
-
# Which would provide you with:
-
#
-
# person = Person.new
-
# person.serializable_hash # => {"name"=>nil}
-
# person.name = "Bob"
-
# person.serializable_hash # => {"name"=>"Bob"}
-
#
-
# An +attributes+ hash must be defined and should contain any attributes you
-
# need to be serialized. Attributes must be strings, not symbols.
-
# When called, serializable hash will use instance methods that match the name
-
# of the attributes hash's keys. In order to override this behavior, take a look
-
# at the private method +read_attribute_for_serialization+.
-
#
-
# Most of the time though, either the JSON or XML serializations are needed.
-
# Both of these modules automatically include the
-
# <tt>ActiveModel::Serialization</tt> module, so there is no need to
-
# explicitly include it.
-
#
-
# A minimal implementation including XML and JSON would be:
-
#
-
# class Person
-
# include ActiveModel::Serializers::JSON
-
# include ActiveModel::Serializers::Xml
-
#
-
# attr_accessor :name
-
#
-
# def attributes
-
# {'name' => nil}
-
# end
-
# end
-
#
-
# Which would provide you with:
-
#
-
# person = Person.new
-
# person.serializable_hash # => {"name"=>nil}
-
# person.as_json # => {"name"=>nil}
-
# person.to_json # => "{\"name\":null}"
-
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
-
#
-
# person.name = "Bob"
-
# person.serializable_hash # => {"name"=>"Bob"}
-
# person.as_json # => {"name"=>"Bob"}
-
# person.to_json # => "{\"name\":\"Bob\"}"
-
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
-
#
-
# Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
-
# <tt>:include</tt>. The following are all valid examples:
-
#
-
# person.serializable_hash(only: 'name')
-
# person.serializable_hash(include: :address)
-
# person.serializable_hash(include: { address: { only: 'city' }})
-
2
module Serialization
-
# Returns a serialized hash of your object.
-
#
-
# class Person
-
# include ActiveModel::Serialization
-
#
-
# attr_accessor :name, :age
-
#
-
# def attributes
-
# {'name' => nil, 'age' => nil}
-
# end
-
#
-
# def capitalized_name
-
# name.capitalize
-
# end
-
# end
-
#
-
# person = Person.new
-
# person.name = 'bob'
-
# person.age = 22
-
# person.serializable_hash # => {"name"=>"bob", "age"=>22}
-
# person.serializable_hash(only: :name) # => {"name"=>"bob"}
-
# person.serializable_hash(except: :name) # => {"age"=>22}
-
# person.serializable_hash(methods: :capitalized_name)
-
# # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
-
2
def serializable_hash(options = nil)
-
options ||= {}
-
-
attribute_names = attributes.keys
-
if only = options[:only]
-
attribute_names &= Array(only).map(&:to_s)
-
elsif except = options[:except]
-
attribute_names -= Array(except).map(&:to_s)
-
end
-
-
hash = {}
-
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
-
-
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) }
-
-
serializable_add_includes(options) do |association, records, opts|
-
hash[association.to_s] = if records.respond_to?(:to_ary)
-
records.to_ary.map { |a| a.serializable_hash(opts) }
-
else
-
records.serializable_hash(opts)
-
end
-
end
-
-
hash
-
end
-
-
2
private
-
-
# Hook method defining how an attribute value should be retrieved for
-
# serialization. By default this is assumed to be an instance named after
-
# the attribute. Override this method in subclasses should you need to
-
# retrieve the value for a given attribute differently:
-
#
-
# class MyClass
-
# include ActiveModel::Serialization
-
#
-
# def initialize(data = {})
-
# @data = data
-
# end
-
#
-
# def read_attribute_for_serialization(key)
-
# @data[key]
-
# end
-
# end
-
2
alias :read_attribute_for_serialization :send
-
-
# Add associations specified via the <tt>:include</tt> option.
-
#
-
# Expects a block that takes as arguments:
-
# +association+ - name of the association
-
# +records+ - the association record(s) to be serialized
-
# +opts+ - options for the association records
-
2
def serializable_add_includes(options = {}) #:nodoc:
-
return unless includes = options[:include]
-
-
unless includes.is_a?(Hash)
-
includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
-
end
-
-
includes.each do |association, opts|
-
if records = send(association)
-
yield association, records, opts
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/json'
-
-
2
module ActiveModel
-
2
module Serializers
-
# == Active \Model \JSON \Serializer
-
2
module JSON
-
2
extend ActiveSupport::Concern
-
2
include ActiveModel::Serialization
-
-
2
included do
-
2
extend ActiveModel::Naming
-
-
2
class_attribute :include_root_in_json, instance_writer: false
-
2
self.include_root_in_json = false
-
end
-
-
# Returns a hash representing the model. Some configuration can be
-
# passed through +options+.
-
#
-
# The option <tt>include_root_in_json</tt> controls the top-level behavior
-
# of +as_json+. If +true+, +as_json+ will emit a single root node named
-
# after the object's type. The default value for <tt>include_root_in_json</tt>
-
# option is +false+.
-
#
-
# user = User.find(1)
-
# user.as_json
-
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
-
# # "created_at" => "2006/08/01", "awesome" => true}
-
#
-
# ActiveRecord::Base.include_root_in_json = true
-
#
-
# user.as_json
-
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
-
# # "created_at" => "2006/08/01", "awesome" => true } }
-
#
-
# This behavior can also be achieved by setting the <tt>:root</tt> option
-
# to +true+ as in:
-
#
-
# user = User.find(1)
-
# user.as_json(root: true)
-
# # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
-
# # "created_at" => "2006/08/01", "awesome" => true } }
-
#
-
# Without any +options+, the returned Hash will include all the model's
-
# attributes.
-
#
-
# user = User.find(1)
-
# user.as_json
-
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
-
# # "created_at" => "2006/08/01", "awesome" => true}
-
#
-
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
-
# the attributes included, and work similar to the +attributes+ method.
-
#
-
# user.as_json(only: [:id, :name])
-
# # => { "id" => 1, "name" => "Konata Izumi" }
-
#
-
# user.as_json(except: [:id, :created_at, :age])
-
# # => { "name" => "Konata Izumi", "awesome" => true }
-
#
-
# To include the result of some method calls on the model use <tt>:methods</tt>:
-
#
-
# user.as_json(methods: :permalink)
-
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
-
# # "created_at" => "2006/08/01", "awesome" => true,
-
# # "permalink" => "1-konata-izumi" }
-
#
-
# To include associations use <tt>:include</tt>:
-
#
-
# user.as_json(include: :posts)
-
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
-
# # "created_at" => "2006/08/01", "awesome" => true,
-
# # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
-
# # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
-
#
-
# Second level and higher order associations work as well:
-
#
-
# user.as_json(include: { posts: {
-
# include: { comments: {
-
# only: :body } },
-
# only: :title } })
-
# # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
-
# # "created_at" => "2006/08/01", "awesome" => true,
-
# # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
-
# # "title" => "Welcome to the weblog" },
-
# # { "comments" => [ { "body" => "Don't think too hard" } ],
-
# # "title" => "So I was thinking" } ] }
-
2
def as_json(options = nil)
-
root = if options && options.key?(:root)
-
options[:root]
-
else
-
include_root_in_json
-
end
-
-
if root
-
root = model_name.element if root == true
-
{ root => serializable_hash(options) }
-
else
-
serializable_hash(options)
-
end
-
end
-
-
# Sets the model +attributes+ from a JSON string. Returns +self+.
-
#
-
# class Person
-
# include ActiveModel::Serializers::JSON
-
#
-
# attr_accessor :name, :age, :awesome
-
#
-
# def attributes=(hash)
-
# hash.each do |key, value|
-
# send("#{key}=", value)
-
# end
-
# end
-
#
-
# def attributes
-
# instance_values
-
# end
-
# end
-
#
-
# json = { name: 'bob', age: 22, awesome:true }.to_json
-
# person = Person.new
-
# person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
-
# person.name # => "bob"
-
# person.age # => 22
-
# person.awesome # => true
-
#
-
# The default value for +include_root+ is +false+. You can change it to
-
# +true+ if the given JSON string includes a single root node.
-
#
-
# json = { person: { name: 'bob', age: 22, awesome:true } }.to_json
-
# person = Person.new
-
# person.from_json(json, true) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
-
# person.name # => "bob"
-
# person.age # => 22
-
# person.awesome # => true
-
2
def from_json(json, include_root=include_root_in_json)
-
hash = ActiveSupport::JSON.decode(json)
-
hash = hash.values.first if include_root
-
self.attributes = hash
-
self
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/attribute_accessors'
-
2
require 'active_support/core_ext/array/conversions'
-
2
require 'active_support/core_ext/hash/conversions'
-
2
require 'active_support/core_ext/hash/slice'
-
2
require 'active_support/core_ext/time/acts_like'
-
-
2
module ActiveModel
-
2
module Serializers
-
# == Active Model XML Serializer
-
2
module Xml
-
2
extend ActiveSupport::Concern
-
2
include ActiveModel::Serialization
-
-
2
included do
-
2
extend ActiveModel::Naming
-
end
-
-
2
class Serializer #:nodoc:
-
2
class Attribute #:nodoc:
-
2
attr_reader :name, :value, :type
-
-
2
def initialize(name, serializable, value)
-
@name, @serializable = name, serializable
-
-
if value.acts_like?(:time) && value.respond_to?(:in_time_zone)
-
value = value.in_time_zone
-
end
-
-
@value = value
-
@type = compute_type
-
end
-
-
2
def decorations
-
decorations = {}
-
decorations[:encoding] = 'base64' if type == :binary
-
decorations[:type] = (type == :string) ? nil : type
-
decorations[:nil] = true if value.nil?
-
decorations
-
end
-
-
2
protected
-
-
2
def compute_type
-
return if value.nil?
-
type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
-
type ||= :string if value.respond_to?(:to_str)
-
type ||= :yaml
-
type
-
end
-
end
-
-
2
class MethodAttribute < Attribute #:nodoc:
-
end
-
-
2
attr_reader :options
-
-
2
def initialize(serializable, options = nil)
-
@serializable = serializable
-
@options = options ? options.dup : {}
-
end
-
-
2
def serializable_hash
-
@serializable.serializable_hash(@options.except(:include))
-
end
-
-
2
def serializable_collection
-
methods = Array(options[:methods]).map(&:to_s)
-
serializable_hash.map do |name, value|
-
name = name.to_s
-
if methods.include?(name)
-
self.class::MethodAttribute.new(name, @serializable, value)
-
else
-
self.class::Attribute.new(name, @serializable, value)
-
end
-
end
-
end
-
-
2
def serialize
-
require 'builder' unless defined? ::Builder
-
-
options[:indent] ||= 2
-
options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
-
-
@builder = options[:builder]
-
@builder.instruct! unless options[:skip_instruct]
-
-
root = (options[:root] || @serializable.model_name.element).to_s
-
root = ActiveSupport::XmlMini.rename_key(root, options)
-
-
args = [root]
-
args << { xmlns: options[:namespace] } if options[:namespace]
-
args << { type: options[:type] } if options[:type] && !options[:skip_types]
-
-
@builder.tag!(*args) do
-
add_attributes_and_methods
-
add_includes
-
add_extra_behavior
-
add_procs
-
yield @builder if block_given?
-
end
-
end
-
-
2
private
-
-
2
def add_extra_behavior
-
end
-
-
2
def add_attributes_and_methods
-
serializable_collection.each do |attribute|
-
key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
-
ActiveSupport::XmlMini.to_tag(key, attribute.value,
-
options.merge(attribute.decorations))
-
end
-
end
-
-
2
def add_includes
-
@serializable.send(:serializable_add_includes, options) do |association, records, opts|
-
add_associations(association, records, opts)
-
end
-
end
-
-
# TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
-
2
def add_associations(association, records, opts)
-
merged_options = opts.merge(options.slice(:builder, :indent))
-
merged_options[:skip_instruct] = true
-
-
[:skip_types, :dasherize, :camelize].each do |key|
-
merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil?
-
end
-
-
if records.respond_to?(:to_ary)
-
records = records.to_ary
-
-
tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
-
type = options[:skip_types] ? { } : { type: "array" }
-
association_name = association.to_s.singularize
-
merged_options[:root] = association_name
-
-
if records.empty?
-
@builder.tag!(tag, type)
-
else
-
@builder.tag!(tag, type) do
-
records.each do |record|
-
if options[:skip_types]
-
record_type = {}
-
else
-
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
-
record_type = { type: record_class }
-
end
-
-
record.to_xml merged_options.merge(record_type)
-
end
-
end
-
end
-
else
-
merged_options[:root] = association.to_s
-
-
unless records.class.to_s.underscore == association.to_s
-
merged_options[:type] = records.class.name
-
end
-
-
records.to_xml merged_options
-
end
-
end
-
-
2
def add_procs
-
if procs = options.delete(:procs)
-
Array(procs).each do |proc|
-
if proc.arity == 1
-
proc.call(options)
-
else
-
proc.call(options, @serializable)
-
end
-
end
-
end
-
end
-
end
-
-
# Returns XML representing the model. Configuration can be
-
# passed through +options+.
-
#
-
# Without any +options+, the returned XML string will include all the
-
# model's attributes.
-
#
-
# user = User.find(1)
-
# user.to_xml
-
#
-
# <?xml version="1.0" encoding="UTF-8"?>
-
# <user>
-
# <id type="integer">1</id>
-
# <name>David</name>
-
# <age type="integer">16</age>
-
# <created-at type="dateTime">2011-01-30T22:29:23Z</created-at>
-
# </user>
-
#
-
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the
-
# attributes included, and work similar to the +attributes+ method.
-
#
-
# To include the result of some method calls on the model use <tt>:methods</tt>.
-
#
-
# To include associations use <tt>:include</tt>.
-
#
-
# For further documentation, see <tt>ActiveRecord::Serialization#to_xml</tt>
-
2
def to_xml(options = {}, &block)
-
Serializer.new(self, options).serialize(&block)
-
end
-
-
# Sets the model +attributes+ from an XML string. Returns +self+.
-
#
-
# class Person
-
# include ActiveModel::Serializers::Xml
-
#
-
# attr_accessor :name, :age, :awesome
-
#
-
# def attributes=(hash)
-
# hash.each do |key, value|
-
# instance_variable_set("@#{key}", value)
-
# end
-
# end
-
#
-
# def attributes
-
# instance_values
-
# end
-
# end
-
#
-
# xml = { name: 'bob', age: 22, awesome:true }.to_xml
-
# person = Person.new
-
# person.from_xml(xml) # => #<Person:0x007fec5e3b3c40 @age=22, @awesome=true, @name="bob">
-
# person.name # => "bob"
-
# person.age # => 22
-
# person.awesome # => true
-
2
def from_xml(xml)
-
self.attributes = Hash.from_xml(xml).values.first
-
self
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
-
# == Active \Model \Translation
-
#
-
# Provides integration between your object and the Rails internationalization
-
# (i18n) framework.
-
#
-
# A minimal implementation could be:
-
#
-
# class TranslatedPerson
-
# extend ActiveModel::Translation
-
# end
-
#
-
# TranslatedPerson.human_attribute_name('my_attribute')
-
# # => "My attribute"
-
#
-
# This also provides the required class methods for hooking into the
-
# Rails internationalization API, including being able to define a
-
# class based +i18n_scope+ and +lookup_ancestors+ to find translations in
-
# parent classes.
-
2
module Translation
-
2
include ActiveModel::Naming
-
-
# Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
-
2
def i18n_scope
-
:activemodel
-
end
-
-
# When localizing a string, it goes through the lookup returned by this
-
# method, which is used in ActiveModel::Name#human,
-
# ActiveModel::Errors#full_messages and
-
# ActiveModel::Translation#human_attribute_name.
-
2
def lookup_ancestors
-
self.ancestors.select { |x| x.respond_to?(:model_name) }
-
end
-
-
# Transforms attribute names into a more human format, such as "First name"
-
# instead of "first_name".
-
#
-
# Person.human_attribute_name("first_name") # => "First name"
-
#
-
# Specify +options+ with additional translating options.
-
2
def human_attribute_name(attribute, options = {})
-
58
options = { count: 1 }.merge!(options)
-
58
parts = attribute.to_s.split(".")
-
58
attribute = parts.pop
-
58
namespace = parts.join("/") unless parts.empty?
-
58
attributes_scope = "#{self.i18n_scope}.attributes"
-
-
58
if namespace
-
defaults = lookup_ancestors.map do |klass|
-
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
-
end
-
defaults << :"#{attributes_scope}.#{namespace}.#{attribute}"
-
else
-
58
defaults = lookup_ancestors.map do |klass|
-
58
:"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}"
-
end
-
end
-
-
58
defaults << :"attributes.#{attribute}"
-
58
defaults << options.delete(:default) if options[:default]
-
58
defaults << attribute.humanize
-
-
58
options[:default] = defaults
-
58
I18n.translate(defaults.shift, options)
-
end
-
end
-
end
-
2
require 'active_support/core_ext/array/extract_options'
-
2
require 'active_support/core_ext/hash/keys'
-
2
require 'active_support/core_ext/hash/except'
-
-
2
module ActiveModel
-
-
# == Active \Model \Validations
-
#
-
# Provides a full validation framework to your objects.
-
#
-
# A minimal implementation could be:
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :first_name, :last_name
-
#
-
# validates_each :first_name, :last_name do |record, attr, value|
-
# record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
-
# end
-
# end
-
#
-
# Which provides you with the full standard validation stack that you
-
# know from Active Record:
-
#
-
# person = Person.new
-
# person.valid? # => true
-
# person.invalid? # => false
-
#
-
# person.first_name = 'zoolander'
-
# person.valid? # => false
-
# person.invalid? # => true
-
# person.errors.messages # => {first_name:["starts with z."]}
-
#
-
# Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+
-
# method to your instances initialized with a new <tt>ActiveModel::Errors</tt>
-
# object, so there is no need for you to do this manually.
-
2
module Validations
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
extend ActiveModel::Naming
-
2
extend ActiveModel::Callbacks
-
2
extend ActiveModel::Translation
-
-
2
extend HelperMethods
-
2
include HelperMethods
-
-
2
attr_accessor :validation_context
-
2
private :validation_context=
-
2
define_callbacks :validate, scope: :name
-
-
2
class_attribute :_validators, instance_writer: false
-
18
self._validators = Hash.new { |h,k| h[k] = [] }
-
end
-
-
2
module ClassMethods
-
# Validates each attribute against a block.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :first_name, :last_name
-
#
-
# validates_each :first_name, :last_name, allow_blank: true do |record, attr, value|
-
# record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
-
# end
-
# end
-
#
-
# Options:
-
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
-
# Runs in all validation contexts by default (nil). You can pass a symbol
-
# or an array of symbols. (e.g. <tt>on: :create</tt> or
-
# <tt>on: :custom_validation_context</tt> or
-
# <tt>on: [:create, :custom_validation_context]</tt>)
-
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
-
# * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
-
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
-
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
-
# proc or string should return or evaluate to a +true+ or +false+ value.
-
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
-
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
-
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
-
# method, proc or string should return or evaluate to a +true+ or +false+
-
# value.
-
2
def validates_each(*attr_names, &block)
-
validates_with BlockValidator, _merge_attributes(attr_names), &block
-
end
-
-
2
VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
-
-
# Adds a validation method or block to the class. This is useful when
-
# overriding the +validate+ instance method becomes too unwieldy and
-
# you're looking for more descriptive declaration of your validations.
-
#
-
# This can be done with a symbol pointing to a method:
-
#
-
# class Comment
-
# include ActiveModel::Validations
-
#
-
# validate :must_be_friends
-
#
-
# def must_be_friends
-
# errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
-
# end
-
# end
-
#
-
# With a block which is passed with the current record to be validated:
-
#
-
# class Comment
-
# include ActiveModel::Validations
-
#
-
# validate do |comment|
-
# comment.must_be_friends
-
# end
-
#
-
# def must_be_friends
-
# errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
-
# end
-
# end
-
#
-
# Or with a block where self points to the current record to be validated:
-
#
-
# class Comment
-
# include ActiveModel::Validations
-
#
-
# validate do
-
# errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
-
# end
-
# end
-
#
-
# Note that the return value of validation methods is not relevant.
-
# It's not possible to halt the validate callback chain.
-
#
-
# Options:
-
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
-
# Runs in all validation contexts by default (nil). You can pass a symbol
-
# or an array of symbols. (e.g. <tt>on: :create</tt> or
-
# <tt>on: :custom_validation_context</tt> or
-
# <tt>on: [:create, :custom_validation_context]</tt>)
-
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
-
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
-
# proc or string should return or evaluate to a +true+ or +false+ value.
-
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
-
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
-
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
-
# method, proc or string should return or evaluate to a +true+ or +false+
-
# value.
-
2
def validate(*args, &block)
-
40
options = args.extract_options!
-
-
80
if args.all? { |arg| arg.is_a?(Symbol) }
-
18
options.each_key do |k|
-
unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
-
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?")
-
end
-
end
-
end
-
-
40
if options.key?(:on)
-
options = options.dup
-
options[:if] = Array(options[:if])
-
options[:if].unshift ->(o) {
-
Array(options[:on]).include?(o.validation_context)
-
}
-
end
-
-
40
args << options
-
40
set_callback(:validate, *args, &block)
-
end
-
-
# List all validators that are being used to validate the model using
-
# +validates_with+ method.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# validates_with MyValidator
-
# validates_with OtherValidator, on: :create
-
# validates_with StrictValidator, strict: true
-
# end
-
#
-
# Person.validators
-
# # => [
-
# # #<MyValidator:0x007fbff403e808 @options={}>,
-
# # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
-
# # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
-
# # ]
-
2
def validators
-
_validators.values.flatten.uniq
-
end
-
-
# Clears all of the validators and validations.
-
#
-
# Note that this will clear anything that is being used to validate
-
# the model for both the +validates_with+ and +validate+ methods.
-
# It clears the validators that are created with an invocation of
-
# +validates_with+ and the callbacks that are set by an invocation
-
# of +validate+.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# validates_with MyValidator
-
# validates_with OtherValidator, on: :create
-
# validates_with StrictValidator, strict: true
-
# validate :cannot_be_robot
-
#
-
# def cannot_be_robot
-
# errors.add(:base, 'A person cannot be a robot') if person_is_robot
-
# end
-
# end
-
#
-
# Person.validators
-
# # => [
-
# # #<MyValidator:0x007fbff403e808 @options={}>,
-
# # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
-
# # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
-
# # ]
-
#
-
# If one runs <tt>Person.clear_validators!</tt> and then checks to see what
-
# validators this class has, you would obtain:
-
#
-
# Person.validators # => []
-
#
-
# Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased
-
# so that:
-
#
-
# Person._validate_callbacks.empty? # => true
-
#
-
2
def clear_validators!
-
reset_callbacks(:validate)
-
_validators.clear
-
end
-
-
# List all validators that are being used to validate a specific attribute.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name , :age
-
#
-
# validates_presence_of :name
-
# validates_inclusion_of :age, in: 0..99
-
# end
-
#
-
# Person.validators_on(:name)
-
# # => [
-
# # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>,
-
# # ]
-
2
def validators_on(*attributes)
-
attributes.flat_map do |attribute|
-
_validators[attribute.to_sym]
-
end
-
end
-
-
# Returns +true+ if +attribute+ is an attribute method, +false+ otherwise.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
# end
-
#
-
# User.attribute_method?(:name) # => true
-
# User.attribute_method?(:age) # => false
-
2
def attribute_method?(attribute)
-
method_defined?(attribute)
-
end
-
-
# Copy validators on inheritance.
-
2
def inherited(base) #:nodoc:
-
12
dup = _validators.dup
-
12
base._validators = dup.each { |k, v| dup[k] = v.dup }
-
12
super
-
end
-
end
-
-
# Clean the +Errors+ object if instance is duped.
-
2
def initialize_dup(other) #:nodoc:
-
@errors = nil
-
super
-
end
-
-
# Returns the +Errors+ object that holds all information about attribute
-
# error messages.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
# validates_presence_of :name
-
# end
-
#
-
# person = Person.new
-
# person.valid? # => false
-
# person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={name:["can't be blank"]}>
-
2
def errors
-
230
@errors ||= Errors.new(self)
-
end
-
-
# Runs all the specified validations and returns +true+ if no errors were
-
# added otherwise +false+.
-
#
-
# Aliased as validate.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
# validates_presence_of :name
-
# end
-
#
-
# person = Person.new
-
# person.name = ''
-
# person.valid? # => false
-
# person.name = 'david'
-
# person.valid? # => true
-
#
-
# Context can optionally be supplied to define which callbacks to test
-
# against (the context is defined on the validations using <tt>:on</tt>).
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
# validates_presence_of :name, on: :new
-
# end
-
#
-
# person = Person.new
-
# person.valid? # => true
-
# person.valid?(:new) # => false
-
2
def valid?(context = nil)
-
21
current_context, self.validation_context = validation_context, context
-
21
errors.clear
-
21
run_validations!
-
ensure
-
21
self.validation_context = current_context
-
end
-
-
2
alias_method :validate, :valid?
-
-
# Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
-
# added, +false+ otherwise.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
# validates_presence_of :name
-
# end
-
#
-
# person = Person.new
-
# person.name = ''
-
# person.invalid? # => true
-
# person.name = 'david'
-
# person.invalid? # => false
-
#
-
# Context can optionally be supplied to define which callbacks to test
-
# against (the context is defined on the validations using <tt>:on</tt>).
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
# validates_presence_of :name, on: :new
-
# end
-
#
-
# person = Person.new
-
# person.invalid? # => false
-
# person.invalid?(:new) # => true
-
2
def invalid?(context = nil)
-
6
!valid?(context)
-
end
-
-
# Hook method defining how an attribute value should be retrieved. By default
-
# this is assumed to be an instance named after the attribute. Override this
-
# method in subclasses should you need to retrieve the value for a given
-
# attribute differently:
-
#
-
# class MyClass
-
# include ActiveModel::Validations
-
#
-
# def initialize(data = {})
-
# @data = data
-
# end
-
#
-
# def read_attribute_for_validation(key)
-
# @data[key]
-
# end
-
# end
-
2
alias :read_attribute_for_validation :send
-
-
2
protected
-
-
2
def run_validations! #:nodoc:
-
21
_run_validate_callbacks
-
21
errors.empty?
-
end
-
end
-
end
-
-
28
Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file }
-
2
module ActiveModel
-
2
module Validations
-
# == Active Model Absence Validator
-
2
class AbsenceValidator < EachValidator #:nodoc:
-
2
def validate_each(record, attr_name, value)
-
record.errors.add(attr_name, :present, options) if value.present?
-
end
-
end
-
-
2
module HelperMethods
-
# Validates that the specified attributes are blank (as defined by
-
# Object#blank?). Happens by default on save.
-
#
-
# class Person < ActiveRecord::Base
-
# validates_absence_of :first_name
-
# end
-
#
-
# The first_name attribute must be in the object and it must be blank.
-
#
-
# Configuration options:
-
# * <tt>:message</tt> - A custom error message (default is: "must be blank").
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
2
def validates_absence_of(*attr_names)
-
validates_with AbsenceValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
-
2
module Validations
-
2
class AcceptanceValidator < EachValidator # :nodoc:
-
2
def initialize(options)
-
super({ allow_nil: true, accept: "1" }.merge!(options))
-
setup!(options[:class])
-
end
-
-
2
def validate_each(record, attribute, value)
-
unless value == options[:accept]
-
record.errors.add(attribute, :accepted, options.except(:accept, :allow_nil))
-
end
-
end
-
-
2
private
-
2
def setup!(klass)
-
attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
-
attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
-
klass.send(:attr_reader, *attr_readers)
-
klass.send(:attr_writer, *attr_writers)
-
end
-
end
-
-
2
module HelperMethods
-
# Encapsulates the pattern of wanting to validate the acceptance of a
-
# terms of service check box (or similar agreement).
-
#
-
# class Person < ActiveRecord::Base
-
# validates_acceptance_of :terms_of_service
-
# validates_acceptance_of :eula, message: 'must be abided'
-
# end
-
#
-
# If the database column does not exist, the +terms_of_service+ attribute
-
# is entirely virtual. This check is performed only if +terms_of_service+
-
# is not +nil+ and by default on save.
-
#
-
# Configuration options:
-
# * <tt>:message</tt> - A custom error message (default is: "must be
-
# accepted").
-
# * <tt>:accept</tt> - Specifies value that is considered accepted.
-
# The default value is a string "1", which makes it easy to relate to
-
# an HTML checkbox. This should be set to +true+ if you are validating
-
# a database column, since the attribute is typecast from "1" to +true+
-
# before validation.
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information.
-
2
def validates_acceptance_of(*attr_names)
-
validates_with AcceptanceValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
2
module Validations
-
# == Active \Model \Validation \Callbacks
-
#
-
# Provides an interface for any class to have +before_validation+ and
-
# +after_validation+ callbacks.
-
#
-
# First, include ActiveModel::Validations::Callbacks from the class you are
-
# creating:
-
#
-
# class MyModel
-
# include ActiveModel::Validations::Callbacks
-
#
-
# before_validation :do_stuff_before_validation
-
# after_validation :do_stuff_after_validation
-
# end
-
#
-
# Like other <tt>before_*</tt> callbacks if +before_validation+ returns
-
# +false+ then <tt>valid?</tt> will not be called.
-
2
module Callbacks
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
include ActiveSupport::Callbacks
-
2
define_callbacks :validation,
-
terminator: ->(_,result) { result == false },
-
skip_after_callbacks_if_terminated: true,
-
scope: [:kind, :name]
-
end
-
-
2
module ClassMethods
-
# Defines a callback that will get called right before validation
-
# happens.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# include ActiveModel::Validations::Callbacks
-
#
-
# attr_accessor :name
-
#
-
# validates_length_of :name, maximum: 6
-
#
-
# before_validation :remove_whitespaces
-
#
-
# private
-
#
-
# def remove_whitespaces
-
# name.strip!
-
# end
-
# end
-
#
-
# person = Person.new
-
# person.name = ' bob '
-
# person.valid? # => true
-
# person.name # => "bob"
-
2
def before_validation(*args, &block)
-
4
options = args.last
-
4
if options.is_a?(Hash) && options[:on]
-
options[:if] = Array(options[:if])
-
options[:on] = Array(options[:on])
-
options[:if].unshift ->(o) {
-
options[:on].include? o.validation_context
-
}
-
end
-
4
set_callback(:validation, :before, *args, &block)
-
end
-
-
# Defines a callback that will get called right after validation
-
# happens.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# include ActiveModel::Validations::Callbacks
-
#
-
# attr_accessor :name, :status
-
#
-
# validates_presence_of :name
-
#
-
# after_validation :set_status
-
#
-
# private
-
#
-
# def set_status
-
# self.status = errors.empty?
-
# end
-
# end
-
#
-
# person = Person.new
-
# person.name = ''
-
# person.valid? # => false
-
# person.status # => false
-
# person.name = 'bob'
-
# person.valid? # => true
-
# person.status # => true
-
2
def after_validation(*args, &block)
-
options = args.extract_options!
-
options[:prepend] = true
-
options[:if] = Array(options[:if])
-
if options[:on]
-
options[:on] = Array(options[:on])
-
options[:if].unshift ->(o) {
-
options[:on].include? o.validation_context
-
}
-
end
-
set_callback(:validation, :after, *(args << options), &block)
-
end
-
end
-
-
2
protected
-
-
# Overwrite run validations to include callbacks.
-
2
def run_validations! #:nodoc:
-
42
_run_validation_callbacks { super }
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/range'
-
-
2
module ActiveModel
-
2
module Validations
-
2
module Clusivity #:nodoc:
-
2
ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \
-
"and must be supplied as the :in (or :within) option of the configuration hash"
-
-
2
def check_validity!
-
unless delimiter.respond_to?(:include?) || delimiter.respond_to?(:call) || delimiter.respond_to?(:to_sym)
-
raise ArgumentError, ERROR_MESSAGE
-
end
-
end
-
-
2
private
-
-
2
def include?(record, value)
-
members = if delimiter.respond_to?(:call)
-
delimiter.call(record)
-
elsif delimiter.respond_to?(:to_sym)
-
record.send(delimiter)
-
else
-
delimiter
-
end
-
-
members.send(inclusion_method(members), value)
-
end
-
-
2
def delimiter
-
@delimiter ||= options[:in] || options[:within]
-
end
-
-
# In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
-
# possible values in the range for equality, which is slower but more accurate.
-
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
-
# endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
-
2
def inclusion_method(enumerable)
-
if enumerable.is_a? Range
-
case enumerable.first
-
when Numeric, Time, DateTime
-
:cover?
-
else
-
:include?
-
end
-
else
-
:include?
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
-
2
module Validations
-
2
class ConfirmationValidator < EachValidator # :nodoc:
-
2
def initialize(options)
-
2
super
-
2
setup!(options[:class])
-
end
-
-
2
def validate_each(record, attribute, value)
-
if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed)
-
human_attribute_name = record.class.human_attribute_name(attribute)
-
record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name))
-
end
-
end
-
-
2
private
-
2
def setup!(klass)
-
2
klass.send(:attr_reader, *attributes.map do |attribute|
-
2
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
-
end.compact)
-
-
2
klass.send(:attr_writer, *attributes.map do |attribute|
-
2
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
-
end.compact)
-
end
-
end
-
-
2
module HelperMethods
-
# Encapsulates the pattern of wanting to validate a password or email
-
# address field with a confirmation.
-
#
-
# Model:
-
# class Person < ActiveRecord::Base
-
# validates_confirmation_of :user_name, :password
-
# validates_confirmation_of :email_address,
-
# message: 'should match confirmation'
-
# end
-
#
-
# View:
-
# <%= password_field "person", "password" %>
-
# <%= password_field "person", "password_confirmation" %>
-
#
-
# The added +password_confirmation+ attribute is virtual; it exists only
-
# as an in-memory attribute for validating the password. To achieve this,
-
# the validation adds accessors to the model for the confirmation
-
# attribute.
-
#
-
# NOTE: This check is performed only if +password_confirmation+ is not
-
# +nil+. To require confirmation, make sure to add a presence check for
-
# the confirmation attribute:
-
#
-
# validates_presence_of :password_confirmation, if: :password_changed?
-
#
-
# Configuration options:
-
# * <tt>:message</tt> - A custom error message (default is: "doesn't match
-
# <tt>%{translated_attribute_name}</tt>").
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
2
def validates_confirmation_of(*attr_names)
-
2
validates_with ConfirmationValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
require "active_model/validations/clusivity"
-
-
2
module ActiveModel
-
-
2
module Validations
-
2
class ExclusionValidator < EachValidator # :nodoc:
-
2
include Clusivity
-
-
2
def validate_each(record, attribute, value)
-
if include?(record, value)
-
record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(value: value))
-
end
-
end
-
end
-
-
2
module HelperMethods
-
# Validates that the value of the specified attribute is not in a
-
# particular enumerable object.
-
#
-
# class Person < ActiveRecord::Base
-
# validates_exclusion_of :username, in: %w( admin superuser ), message: "You don't belong here"
-
# validates_exclusion_of :age, in: 30..60, message: 'This site is only for under 30 and over 60'
-
# validates_exclusion_of :format, in: %w( mov avi ), message: "extension %{value} is not allowed"
-
# validates_exclusion_of :password, in: ->(person) { [person.username, person.first_name] },
-
# message: 'should not be the same as your username or first name'
-
# validates_exclusion_of :karma, in: :reserved_karmas
-
# end
-
#
-
# Configuration options:
-
# * <tt>:in</tt> - An enumerable object of items that the value shouldn't
-
# be part of. This can be supplied as a proc, lambda or symbol which returns an
-
# enumerable. If the enumerable is a range the test is performed with
-
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
-
# <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>.
-
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
-
# reserved").
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
2
def validates_exclusion_of(*attr_names)
-
validates_with ExclusionValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
-
2
module Validations
-
2
class FormatValidator < EachValidator # :nodoc:
-
2
def validate_each(record, attribute, value)
-
if options[:with]
-
regexp = option_call(record, :with)
-
record_error(record, attribute, :with, value) if value.to_s !~ regexp
-
elsif options[:without]
-
regexp = option_call(record, :without)
-
record_error(record, attribute, :without, value) if value.to_s =~ regexp
-
end
-
end
-
-
2
def check_validity!
-
2
unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
-
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
-
end
-
-
2
check_options_validity :with
-
2
check_options_validity :without
-
end
-
-
2
private
-
-
2
def option_call(record, name)
-
option = options[name]
-
option.respond_to?(:call) ? option.call(record) : option
-
end
-
-
2
def record_error(record, attribute, name, value)
-
record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
-
end
-
-
2
def check_options_validity(name)
-
4
if option = options[name]
-
2
if option.is_a?(Regexp)
-
2
if options[:multiline] != true && regexp_using_multiline_anchors?(option)
-
raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
-
"which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
-
":multiline => true option?"
-
end
-
elsif !option.respond_to?(:call)
-
raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
-
end
-
end
-
end
-
-
2
def regexp_using_multiline_anchors?(regexp)
-
2
source = regexp.source
-
2
source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
-
end
-
end
-
-
2
module HelperMethods
-
# Validates whether the value of the specified attribute is of the correct
-
# form, going by the regular expression provided. You can require that the
-
# attribute matches the regular expression:
-
#
-
# class Person < ActiveRecord::Base
-
# validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create
-
# end
-
#
-
# Alternatively, you can require that the specified attribute does _not_
-
# match the regular expression:
-
#
-
# class Person < ActiveRecord::Base
-
# validates_format_of :email, without: /NOSPAM/
-
# end
-
#
-
# You can also provide a proc or lambda which will determine the regular
-
# expression that will be used to validate the attribute.
-
#
-
# class Person < ActiveRecord::Base
-
# # Admin can have number as a first letter in their screen name
-
# validates_format_of :screen_name,
-
# with: ->(person) { person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i }
-
# end
-
#
-
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the
-
# string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
-
#
-
# Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass
-
# the <tt>multiline: true</tt> option in case you use any of these two
-
# anchors in the provided regular expression. In most cases, you should be
-
# using <tt>\A</tt> and <tt>\z</tt>.
-
#
-
# You must pass either <tt>:with</tt> or <tt>:without</tt> as an option.
-
# In addition, both must be a regular expression or a proc or lambda, or
-
# else an exception will be raised.
-
#
-
# Configuration options:
-
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
-
# * <tt>:with</tt> - Regular expression that if the attribute matches will
-
# result in a successful validation. This can be provided as a proc or
-
# lambda returning regular expression which will be called at runtime.
-
# * <tt>:without</tt> - Regular expression that if the attribute does not
-
# match will result in a successful validation. This can be provided as
-
# a proc or lambda returning regular expression which will be called at
-
# runtime.
-
# * <tt>:multiline</tt> - Set to true if your regular expression contains
-
# anchors that match the beginning or end of lines as opposed to the
-
# beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>.
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
2
def validates_format_of(*attr_names)
-
2
validates_with FormatValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
require "active_model/validations/clusivity"
-
-
2
module ActiveModel
-
-
2
module Validations
-
2
class InclusionValidator < EachValidator # :nodoc:
-
2
include Clusivity
-
-
2
def validate_each(record, attribute, value)
-
unless include?(record, value)
-
record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(value: value))
-
end
-
end
-
end
-
-
2
module HelperMethods
-
# Validates whether the value of the specified attribute is available in a
-
# particular enumerable object.
-
#
-
# class Person < ActiveRecord::Base
-
# validates_inclusion_of :gender, in: %w( m f )
-
# validates_inclusion_of :age, in: 0..99
-
# validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
-
# validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
-
# validates_inclusion_of :karma, in: :available_karmas
-
# end
-
#
-
# Configuration options:
-
# * <tt>:in</tt> - An enumerable object of available items. This can be
-
# supplied as a proc, lambda or symbol which returns an enumerable. If the
-
# enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>,
-
# otherwise with <tt>include?</tt>. When using a proc or lambda the instance
-
# under validation is passed as an argument.
-
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
-
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
-
# not included in the list").
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
2
def validates_inclusion_of(*attr_names)
-
validates_with InclusionValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
-
# == Active \Model Length Validator
-
2
module Validations
-
2
class LengthValidator < EachValidator # :nodoc:
-
2
MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
-
2
CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
-
-
2
RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
-
-
2
def initialize(options)
-
2
if range = (options.delete(:in) || options.delete(:within))
-
2
raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
-
2
options[:minimum], options[:maximum] = range.min, range.max
-
end
-
-
2
if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?
-
options[:minimum] = 1
-
end
-
-
2
super
-
end
-
-
2
def check_validity!
-
2
keys = CHECKS.keys & options.keys
-
-
2
if keys.empty?
-
raise ArgumentError, 'Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option.'
-
end
-
-
2
keys.each do |key|
-
4
value = options[key]
-
-
4
unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY
-
raise ArgumentError, ":#{key} must be a nonnegative Integer or Infinity"
-
end
-
end
-
end
-
-
2
def validate_each(record, attribute, value)
-
value = tokenize(value)
-
value_length = value.respond_to?(:length) ? value.length : value.to_s.length
-
errors_options = options.except(*RESERVED_OPTIONS)
-
-
CHECKS.each do |key, validity_check|
-
next unless check_value = options[key]
-
-
if !value.nil? || skip_nil_check?(key)
-
next if value_length.send(validity_check, check_value)
-
end
-
-
errors_options[:count] = check_value
-
-
default_message = options[MESSAGES[key]]
-
errors_options[:message] ||= default_message if default_message
-
-
record.errors.add(attribute, MESSAGES[key], errors_options)
-
end
-
end
-
-
2
private
-
-
2
def tokenize(value)
-
if options[:tokenizer] && value.kind_of?(String)
-
options[:tokenizer].call(value)
-
end || value
-
end
-
-
2
def skip_nil_check?(key)
-
key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
-
end
-
end
-
-
2
module HelperMethods
-
-
# Validates that the specified attribute matches the length restrictions
-
# supplied. Only one option can be used at a time:
-
#
-
# class Person < ActiveRecord::Base
-
# validates_length_of :first_name, maximum: 30
-
# validates_length_of :last_name, maximum: 30, message: "less than 30 if you don't mind"
-
# validates_length_of :fax, in: 7..32, allow_nil: true
-
# validates_length_of :phone, in: 7..32, allow_blank: true
-
# validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name'
-
# validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters'
-
# validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me."
-
# validates_length_of :essay, minimum: 100, too_short: 'Your essay must be at least 100 words.',
-
# tokenizer: ->(str) { str.scan(/\w+/) }
-
# end
-
#
-
# Configuration options:
-
# * <tt>:minimum</tt> - The minimum size of the attribute.
-
# * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by
-
# default if not used with :minimum.
-
# * <tt>:is</tt> - The exact size of the attribute.
-
# * <tt>:within</tt> - A range specifying the minimum and maximum size of
-
# the attribute.
-
# * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>.
-
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
-
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
-
# * <tt>:too_long</tt> - The error message if the attribute goes over the
-
# maximum (default is: "is too long (maximum is %{count} characters)").
-
# * <tt>:too_short</tt> - The error message if the attribute goes under the
-
# minimum (default is: "is too short (min is %{count} characters)").
-
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
-
# method and the attribute is the wrong size (default is: "is the wrong
-
# length (should be %{count} characters)").
-
# * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>,
-
# <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate
-
# <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
-
# * <tt>:tokenizer</tt> - Specifies how to split up the attribute string.
-
# (e.g. <tt>tokenizer: ->(str) { str.scan(/\w+/) }</tt> to count words
-
# as in above example). Defaults to <tt>->(value) { value.split(//) }</tt>
-
# which counts individual characters.
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+ and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
2
def validates_length_of(*attr_names)
-
2
validates_with LengthValidator, _merge_attributes(attr_names)
-
end
-
-
2
alias_method :validates_size_of, :validates_length_of
-
end
-
end
-
end
-
2
module ActiveModel
-
-
2
module Validations
-
2
class NumericalityValidator < EachValidator # :nodoc:
-
2
CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
-
equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
-
odd: :odd?, even: :even?, other_than: :!= }.freeze
-
-
2
RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
-
-
2
def check_validity!
-
2
keys = CHECKS.keys - [:odd, :even]
-
2
options.slice(*keys).each do |option, value|
-
2
unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
-
raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
-
end
-
end
-
end
-
-
2
def validate_each(record, attr_name, value)
-
8
before_type_cast = :"#{attr_name}_before_type_cast"
-
-
8
raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast)
-
8
raw_value ||= value
-
-
8
if record_attribute_changed_in_place?(record, attr_name)
-
raw_value = value
-
end
-
-
8
return if options[:allow_nil] && raw_value.nil?
-
-
8
unless value = parse_raw_value_as_a_number(raw_value)
-
1
record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
-
1
return
-
end
-
-
7
if allow_only_integer?(record)
-
unless value = parse_raw_value_as_an_integer(raw_value)
-
record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
-
return
-
end
-
end
-
-
7
options.slice(*CHECKS.keys).each do |option, option_value|
-
7
case option
-
when :odd, :even
-
unless value.to_i.send(CHECKS[option])
-
record.errors.add(attr_name, option, filtered_options(value))
-
end
-
else
-
7
case option_value
-
when Proc
-
option_value = option_value.call(record)
-
when Symbol
-
option_value = record.send(option_value)
-
end
-
-
7
unless value.send(CHECKS[option], option_value)
-
4
record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
-
end
-
end
-
end
-
end
-
-
2
protected
-
-
2
def parse_raw_value_as_a_number(raw_value)
-
8
Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
-
rescue ArgumentError, TypeError
-
1
nil
-
end
-
-
2
def parse_raw_value_as_an_integer(raw_value)
-
raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/
-
end
-
-
2
def filtered_options(value)
-
5
filtered = options.except(*RESERVED_OPTIONS)
-
5
filtered[:value] = value
-
5
filtered
-
end
-
-
2
def allow_only_integer?(record)
-
7
case options[:only_integer]
-
when Symbol
-
record.send(options[:only_integer])
-
when Proc
-
options[:only_integer].call(record)
-
else
-
7
options[:only_integer]
-
end
-
end
-
-
2
private
-
-
2
def record_attribute_changed_in_place?(record, attr_name)
-
record.respond_to?(:attribute_changed_in_place?) &&
-
8
record.attribute_changed_in_place?(attr_name.to_s)
-
end
-
end
-
-
2
module HelperMethods
-
# Validates whether the value of the specified attribute is numeric by
-
# trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
-
# is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\Z/</tt>
-
# (if <tt>only_integer</tt> is set to +true+).
-
#
-
# class Person < ActiveRecord::Base
-
# validates_numericality_of :value, on: :create
-
# end
-
#
-
# Configuration options:
-
# * <tt>:message</tt> - A custom error message (default is: "is not a number").
-
# * <tt>:only_integer</tt> - Specifies whether the value has to be an
-
# integer, e.g. an integral value (default is +false+).
-
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
-
# +false+). Notice that for fixnum and float columns empty strings are
-
# converted to +nil+.
-
# * <tt>:greater_than</tt> - Specifies the value must be greater than the
-
# supplied value.
-
# * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
-
# greater than or equal the supplied value.
-
# * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
-
# value.
-
# * <tt>:less_than</tt> - Specifies the value must be less than the
-
# supplied value.
-
# * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
-
# than or equal the supplied value.
-
# * <tt>:other_than</tt> - Specifies the value must be other than the
-
# supplied value.
-
# * <tt>:odd</tt> - Specifies the value must be an odd number.
-
# * <tt>:even</tt> - Specifies the value must be an even number.
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
#
-
# The following checks can also be supplied with a proc or a symbol which
-
# corresponds to a method:
-
#
-
# * <tt>:greater_than</tt>
-
# * <tt>:greater_than_or_equal_to</tt>
-
# * <tt>:equal_to</tt>
-
# * <tt>:less_than</tt>
-
# * <tt>:less_than_or_equal_to</tt>
-
# * <tt>:only_integer</tt>
-
#
-
# For example:
-
#
-
# class Person < ActiveRecord::Base
-
# validates_numericality_of :width, less_than: ->(person) { person.height }
-
# validates_numericality_of :width, greater_than: :minimum_weight
-
# end
-
2
def validates_numericality_of(*attr_names)
-
validates_with NumericalityValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
-
2
module ActiveModel
-
-
2
module Validations
-
2
class PresenceValidator < EachValidator # :nodoc:
-
2
def validate_each(record, attr_name, value)
-
46
record.errors.add(attr_name, :blank, options) if value.blank?
-
end
-
end
-
-
2
module HelperMethods
-
# Validates that the specified attributes are not blank (as defined by
-
# Object#blank?). Happens by default on save.
-
#
-
# class Person < ActiveRecord::Base
-
# validates_presence_of :first_name
-
# end
-
#
-
# The first_name attribute must be in the object and it cannot be blank.
-
#
-
# If you want to validate the presence of a boolean field (where the real
-
# values are +true+ and +false+), you will want to use
-
# <tt>validates_inclusion_of :field_name, in: [true, false]</tt>.
-
#
-
# This is due to the way Object#blank? handles boolean values:
-
# <tt>false.blank? # => true</tt>.
-
#
-
# Configuration options:
-
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
-
#
-
# There is also a list of default options supported by every validator:
-
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
-
# See <tt>ActiveModel::Validation#validates</tt> for more information
-
2
def validates_presence_of(*attr_names)
-
validates_with PresenceValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/slice'
-
-
2
module ActiveModel
-
2
module Validations
-
2
module ClassMethods
-
# This method is a shortcut to all default validators and any custom
-
# validator classes ending in 'Validator'. Note that Rails default
-
# validators can be overridden inside specific classes by creating
-
# custom validator classes in their place such as PresenceValidator.
-
#
-
# Examples of using the default rails validators:
-
#
-
# validates :terms, acceptance: true
-
# validates :password, confirmation: true
-
# validates :username, exclusion: { in: %w(admin superuser) }
-
# validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }
-
# validates :age, inclusion: { in: 0..9 }
-
# validates :first_name, length: { maximum: 30 }
-
# validates :age, numericality: true
-
# validates :username, presence: true
-
# validates :username, uniqueness: true
-
#
-
# The power of the +validates+ method comes when using custom validators
-
# and default validators in one call for a given attribute.
-
#
-
# class EmailValidator < ActiveModel::EachValidator
-
# def validate_each(record, attribute, value)
-
# record.errors.add attribute, (options[:message] || "is not an email") unless
-
# value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
-
# end
-
# end
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# attr_accessor :name, :email
-
#
-
# validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
-
# validates :email, presence: true, email: true
-
# end
-
#
-
# Validator classes may also exist within the class being validated
-
# allowing custom modules of validators to be included as needed.
-
#
-
# class Film
-
# include ActiveModel::Validations
-
#
-
# class TitleValidator < ActiveModel::EachValidator
-
# def validate_each(record, attribute, value)
-
# record.errors.add attribute, "must start with 'the'" unless value =~ /\Athe/i
-
# end
-
# end
-
#
-
# validates :name, title: true
-
# end
-
#
-
# Additionally validator classes may be in another namespace and still
-
# used within any class.
-
#
-
# validates :name, :'film/title' => true
-
#
-
# The validators hash can also handle regular expressions, ranges, arrays
-
# and strings in shortcut form.
-
#
-
# validates :email, format: /@/
-
# validates :gender, inclusion: %w(male female)
-
# validates :password, length: 6..20
-
#
-
# When using shortcut form, ranges and arrays are passed to your
-
# validator's initializer as <tt>options[:in]</tt> while other types
-
# including regular expressions and strings are passed as <tt>options[:with]</tt>.
-
#
-
# There is also a list of options that could be used along with validators:
-
#
-
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
-
# Runs in all validation contexts by default (nil). You can pass a symbol
-
# or an array of symbols. (e.g. <tt>on: :create</tt> or
-
# <tt>on: :custom_validation_context</tt> or
-
# <tt>on: [:create, :custom_validation_context]</tt>)
-
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
-
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
-
# proc or string should return or evaluate to a +true+ or +false+ value.
-
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
-
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
-
# method, proc or string should return or evaluate to a +true+ or
-
# +false+ value.
-
# * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+.
-
# * <tt>:allow_blank</tt> - Skip validation if the attribute is blank.
-
# * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true
-
# will raise ActiveModel::StrictValidationFailed instead of adding the error.
-
# <tt>:strict</tt> option can also be set to any other exception.
-
#
-
# Example:
-
#
-
# validates :password, presence: true, confirmation: true, if: :password_required?
-
# validates :token, uniqueness: true, strict: TokenGenerationException
-
#
-
#
-
# Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+
-
# and +:message+ can be given to one specific validator, as a hash:
-
#
-
# validates :password, presence: { if: :password_required?, message: 'is forgotten.' }, confirmation: true
-
2
def validates(*attributes)
-
2
defaults = attributes.extract_options!.dup
-
2
validations = defaults.slice!(*_validates_default_keys)
-
-
2
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
-
2
raise ArgumentError, "You need to supply at least one validation" if validations.empty?
-
-
2
defaults[:attributes] = attributes
-
-
2
validations.each do |key, options|
-
2
next unless options
-
2
key = "#{key.to_s.camelize}Validator"
-
-
2
begin
-
2
validator = key.include?('::') ? key.constantize : const_get(key)
-
rescue NameError
-
raise ArgumentError, "Unknown validator: '#{key}'"
-
end
-
-
2
validates_with(validator, defaults.merge(_parse_validates_options(options)))
-
end
-
end
-
-
# This method is used to define validations that cannot be corrected by end
-
# users and are considered exceptional. So each validator defined with bang
-
# or <tt>:strict</tt> option set to <tt>true</tt> will always raise
-
# <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error
-
# when validation fails. See <tt>validates</tt> for more information about
-
# the validation itself.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# attr_accessor :name
-
# validates! :name, presence: true
-
# end
-
#
-
# person = Person.new
-
# person.name = ''
-
# person.valid?
-
# # => ActiveModel::StrictValidationFailed: Name can't be blank
-
2
def validates!(*attributes)
-
options = attributes.extract_options!
-
options[:strict] = true
-
validates(*(attributes << options))
-
end
-
-
2
protected
-
-
# When creating custom validators, it might be useful to be able to specify
-
# additional default keys. This can be done by overwriting this method.
-
2
def _validates_default_keys # :nodoc:
-
2
[:if, :unless, :on, :allow_blank, :allow_nil , :strict]
-
end
-
-
2
def _parse_validates_options(options) # :nodoc:
-
2
case options
-
when TrueClass
-
{}
-
when Hash
-
2
options
-
when Range, Array
-
{ in: options }
-
else
-
{ with: options }
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveModel
-
2
module Validations
-
2
module HelperMethods
-
2
private
-
2
def _merge_attributes(attr_names)
-
20
options = attr_names.extract_options!.symbolize_keys
-
20
attr_names.flatten!
-
20
options[:attributes] = attr_names
-
20
options
-
end
-
end
-
-
2
class WithValidator < EachValidator # :nodoc:
-
2
def validate_each(record, attr, val)
-
method_name = options[:with]
-
-
if record.method(method_name).arity == 0
-
record.send method_name
-
else
-
record.send method_name, attr
-
end
-
end
-
end
-
-
2
module ClassMethods
-
# Passes the record off to the class or classes specified and allows them
-
# to add errors based on more complex conditions.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# validates_with MyValidator
-
# end
-
#
-
# class MyValidator < ActiveModel::Validator
-
# def validate(record)
-
# if some_complex_logic
-
# record.errors.add :base, 'This record is invalid'
-
# end
-
# end
-
#
-
# private
-
# def some_complex_logic
-
# # ...
-
# end
-
# end
-
#
-
# You may also pass it multiple classes, like so:
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# validates_with MyValidator, MyOtherValidator, on: :create
-
# end
-
#
-
# Configuration options:
-
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
-
# Runs in all validation contexts by default (nil). You can pass a symbol
-
# or an array of symbols. (e.g. <tt>on: :create</tt> or
-
# <tt>on: :custom_validation_context</tt> or
-
# <tt>on: [:create, :custom_validation_context]</tt>)
-
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
-
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>).
-
# The method, proc or string should return or evaluate to a +true+ or
-
# +false+ value.
-
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
-
# determine if the validation should not occur
-
# (e.g. <tt>unless: :skip_validation</tt>, or
-
# <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>).
-
# The method, proc or string should return or evaluate to a +true+ or
-
# +false+ value.
-
# * <tt>:strict</tt> - Specifies whether validation should be strict.
-
# See <tt>ActiveModel::Validation#validates!</tt> for more information.
-
#
-
# If you pass any additional configuration options, they will be passed
-
# to the class and available as +options+:
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# validates_with MyValidator, my_custom_key: 'my custom value'
-
# end
-
#
-
# class MyValidator < ActiveModel::Validator
-
# def validate(record)
-
# options[:my_custom_key] # => "my custom value"
-
# end
-
# end
-
2
def validates_with(*args, &block)
-
22
options = args.extract_options!
-
22
options[:class] = self
-
-
22
args.each do |klass|
-
22
validator = klass.new(options, &block)
-
-
22
if validator.respond_to?(:attributes) && !validator.attributes.empty?
-
22
validator.attributes.each do |attribute|
-
30
_validators[attribute.to_sym] << validator
-
end
-
else
-
_validators[nil] << validator
-
end
-
-
22
validate(validator, options)
-
end
-
end
-
end
-
-
# Passes the record off to the class or classes specified and allows them
-
# to add errors based on more complex conditions.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# validate :instance_validations
-
#
-
# def instance_validations
-
# validates_with MyValidator
-
# end
-
# end
-
#
-
# Please consult the class method documentation for more information on
-
# creating your own validator.
-
#
-
# You may also pass it multiple classes, like so:
-
#
-
# class Person
-
# include ActiveModel::Validations
-
#
-
# validate :instance_validations, on: :create
-
#
-
# def instance_validations
-
# validates_with MyValidator, MyOtherValidator
-
# end
-
# end
-
#
-
# Standard configuration options (<tt>:on</tt>, <tt>:if</tt> and
-
# <tt>:unless</tt>), which are available on the class version of
-
# +validates_with+, should instead be placed on the +validates+ method
-
# as these are applied and tested in the callback.
-
#
-
# If you pass any additional configuration options, they will be passed
-
# to the class and available as +options+, please refer to the
-
# class version of this method for more information.
-
2
def validates_with(*args, &block)
-
options = args.extract_options!
-
options[:class] = self.class
-
-
args.each do |klass|
-
validator = klass.new(options, &block)
-
validator.validate(self)
-
end
-
end
-
end
-
end
-
2
require "active_support/core_ext/module/anonymous"
-
-
2
module ActiveModel
-
-
# == Active \Model \Validator
-
#
-
# A simple base class that can be used along with
-
# ActiveModel::Validations::ClassMethods.validates_with
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# validates_with MyValidator
-
# end
-
#
-
# class MyValidator < ActiveModel::Validator
-
# def validate(record)
-
# if some_complex_logic
-
# record.errors[:base] = "This record is invalid"
-
# end
-
# end
-
#
-
# private
-
# def some_complex_logic
-
# # ...
-
# end
-
# end
-
#
-
# Any class that inherits from ActiveModel::Validator must implement a method
-
# called +validate+ which accepts a +record+.
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# validates_with MyValidator
-
# end
-
#
-
# class MyValidator < ActiveModel::Validator
-
# def validate(record)
-
# record # => The person instance being validated
-
# options # => Any non-standard options passed to validates_with
-
# end
-
# end
-
#
-
# To cause a validation error, you must add to the +record+'s errors directly
-
# from within the validators message.
-
#
-
# class MyValidator < ActiveModel::Validator
-
# def validate(record)
-
# record.errors.add :base, "This is some custom error message"
-
# record.errors.add :first_name, "This is some complex validation"
-
# # etc...
-
# end
-
# end
-
#
-
# To add behavior to the initialize method, use the following signature:
-
#
-
# class MyValidator < ActiveModel::Validator
-
# def initialize(options)
-
# super
-
# @my_custom_field = options[:field_name] || :first_name
-
# end
-
# end
-
#
-
# Note that the validator is initialized only once for the whole application
-
# life cycle, and not on each validation run.
-
#
-
# The easiest way to add custom validators for validating individual attributes
-
# is with the convenient <tt>ActiveModel::EachValidator</tt>.
-
#
-
# class TitleValidator < ActiveModel::EachValidator
-
# def validate_each(record, attribute, value)
-
# record.errors.add attribute, 'must be Mr., Mrs., or Dr.' unless %w(Mr. Mrs. Dr.).include?(value)
-
# end
-
# end
-
#
-
# This can now be used in combination with the +validates+ method
-
# (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this).
-
#
-
# class Person
-
# include ActiveModel::Validations
-
# attr_accessor :title
-
#
-
# validates :title, presence: true, title: true
-
# end
-
#
-
# It can be useful to access the class that is using that validator when there are prerequisites such
-
# as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor.
-
# To setup your validator override the constructor.
-
#
-
# class MyValidator < ActiveModel::Validator
-
# def initialize(options={})
-
# super
-
# options[:class].send :attr_accessor, :custom_attribute
-
# end
-
# end
-
2
class Validator
-
2
attr_reader :options
-
-
# Returns the kind of the validator.
-
#
-
# PresenceValidator.kind # => :presence
-
# UniquenessValidator.kind # => :uniqueness
-
2
def self.kind
-
@kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous?
-
end
-
-
# Accepts options that will be made available through the +options+ reader.
-
2
def initialize(options = {})
-
22
@options = options.except(:class).freeze
-
end
-
-
# Returns the kind for this validator.
-
#
-
# PresenceValidator.new.kind # => :presence
-
# UniquenessValidator.new.kind # => :uniqueness
-
2
def kind
-
self.class.kind
-
end
-
-
# Override this method in subclasses with validation logic, adding errors
-
# to the records +errors+ array where necessary.
-
2
def validate(record)
-
raise NotImplementedError, "Subclasses must implement a validate(record) method."
-
end
-
end
-
-
# +EachValidator+ is a validator which iterates through the attributes given
-
# in the options hash invoking the <tt>validate_each</tt> method passing in the
-
# record, attribute and value.
-
#
-
# All Active Model validations are built on top of this validator.
-
2
class EachValidator < Validator #:nodoc:
-
2
attr_reader :attributes
-
-
# Returns a new validator instance. All options will be available via the
-
# +options+ reader, however the <tt>:attributes</tt> option will be removed
-
# and instead be made available through the +attributes+ reader.
-
2
def initialize(options)
-
22
@attributes = Array(options.delete(:attributes))
-
22
raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
-
22
super
-
22
check_validity!
-
end
-
-
# Performs validation on the supplied record. By default this will call
-
# +validates_each+ to determine validity therefore subclasses should
-
# override +validates_each+ with validation logic.
-
2
def validate(record)
-
38
attributes.each do |attribute|
-
69
value = record.read_attribute_for_validation(attribute)
-
69
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
-
69
validate_each(record, attribute, value)
-
end
-
end
-
-
# Override this method in subclasses with the validation logic, adding
-
# errors to the records +errors+ array where necessary.
-
2
def validate_each(record, attribute, value)
-
raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method"
-
end
-
-
# Hook method that gets called by the initializer allowing verification
-
# that the arguments supplied are valid. You could for example raise an
-
# +ArgumentError+ when invalid options are supplied.
-
2
def check_validity!
-
end
-
end
-
-
# +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
-
# and call this block for each attribute being validated. +validates_each+ uses this validator.
-
2
class BlockValidator < EachValidator #:nodoc:
-
2
def initialize(options, &block)
-
@block = block
-
super
-
end
-
-
2
private
-
-
2
def validate_each(record, attribute, value)
-
@block.call(record, attribute, value)
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record Aggregations
-
2
module Aggregations # :nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
def clear_aggregation_cache #:nodoc:
-
@aggregation_cache.clear if persisted?
-
end
-
-
# Active Record implements aggregation through a macro-like class method called +composed_of+
-
# for representing attributes as value objects. It expresses relationships like "Account [is]
-
# composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
-
# to the macro adds a description of how the value objects are created from the attributes of
-
# the entity object (when the entity is initialized either as a new object or from finding an
-
# existing object) and how it can be turned back into attributes (when the entity is saved to
-
# the database).
-
#
-
# class Customer < ActiveRecord::Base
-
# composed_of :balance, class_name: "Money", mapping: %w(balance amount)
-
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
-
# end
-
#
-
# The customer class now has the following methods to manipulate the value objects:
-
# * <tt>Customer#balance, Customer#balance=(money)</tt>
-
# * <tt>Customer#address, Customer#address=(address)</tt>
-
#
-
# These methods will operate with value objects like the ones described below:
-
#
-
# class Money
-
# include Comparable
-
# attr_reader :amount, :currency
-
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
-
#
-
# def initialize(amount, currency = "USD")
-
# @amount, @currency = amount, currency
-
# end
-
#
-
# def exchange_to(other_currency)
-
# exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
-
# Money.new(exchanged_amount, other_currency)
-
# end
-
#
-
# def ==(other_money)
-
# amount == other_money.amount && currency == other_money.currency
-
# end
-
#
-
# def <=>(other_money)
-
# if currency == other_money.currency
-
# amount <=> other_money.amount
-
# else
-
# amount <=> other_money.exchange_to(currency).amount
-
# end
-
# end
-
# end
-
#
-
# class Address
-
# attr_reader :street, :city
-
# def initialize(street, city)
-
# @street, @city = street, city
-
# end
-
#
-
# def close_to?(other_address)
-
# city == other_address.city
-
# end
-
#
-
# def ==(other_address)
-
# city == other_address.city && street == other_address.street
-
# end
-
# end
-
#
-
# Now it's possible to access attributes from the database through the value objects instead. If
-
# you choose to name the composition the same as the attribute's name, it will be the only way to
-
# access that attribute. That's the case with our +balance+ attribute. You interact with the value
-
# objects just like you would with any other attribute:
-
#
-
# customer.balance = Money.new(20) # sets the Money value object and the attribute
-
# customer.balance # => Money value object
-
# customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
-
# customer.balance > Money.new(10) # => true
-
# customer.balance == Money.new(20) # => true
-
# customer.balance < Money.new(5) # => false
-
#
-
# Value objects can also be composed of multiple attributes, such as the case of Address. The order
-
# of the mappings will determine the order of the parameters.
-
#
-
# customer.address_street = "Hyancintvej"
-
# customer.address_city = "Copenhagen"
-
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
-
#
-
# customer.address_street = "Vesterbrogade"
-
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
-
# customer.clear_aggregation_cache
-
# customer.address # => Address.new("Vesterbrogade", "Copenhagen")
-
#
-
# customer.address = Address.new("May Street", "Chicago")
-
# customer.address_street # => "May Street"
-
# customer.address_city # => "Chicago"
-
#
-
# == Writing value objects
-
#
-
# Value objects are immutable and interchangeable objects that represent a given value, such as
-
# a Money object representing $5. Two Money objects both representing $5 should be equal (through
-
# methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
-
# unlike entity objects where equality is determined by identity. An entity class such as Customer can
-
# easily have two different objects that both have an address on Hyancintvej. Entity identity is
-
# determined by object or relational unique identifiers (such as primary keys). Normal
-
# ActiveRecord::Base classes are entity objects.
-
#
-
# It's also important to treat the value objects as immutable. Don't allow the Money object to have
-
# its amount changed after creation. Create a new Money object with the new value instead. The
-
# Money#exchange_to method is an example of this. It returns a new value object instead of changing
-
# its own values. Active Record won't persist value objects that have been changed through means
-
# other than the writer method.
-
#
-
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value
-
# object. Attempting to change it afterwards will result in a RuntimeError.
-
#
-
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
-
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
-
#
-
# == Custom constructors and converters
-
#
-
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value
-
# class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
-
# option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
-
# a custom constructor to be specified.
-
#
-
# When a new value is assigned to the value object, the default assumption is that the new value
-
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
-
# converted to an instance of value class if necessary.
-
#
-
# For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
-
# aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html).
-
# The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
-
# New values can be assigned to the value object using either another NetAddr::CIDR object, a string
-
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
-
# these requirements:
-
#
-
# class NetworkResource < ActiveRecord::Base
-
# composed_of :cidr,
-
# class_name: 'NetAddr::CIDR',
-
# mapping: [ %w(network_address network), %w(cidr_range bits) ],
-
# allow_nil: true,
-
# constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
-
# converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
-
# end
-
#
-
# # This calls the :constructor
-
# network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)
-
#
-
# # These assignments will both use the :converter
-
# network_resource.cidr = [ '192.168.2.1', 8 ]
-
# network_resource.cidr = '192.168.0.1/24'
-
#
-
# # This assignment won't use the :converter as the value is already an instance of the value class
-
# network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
-
#
-
# # Saving and then reloading will use the :constructor on reload
-
# network_resource.save
-
# network_resource.reload
-
#
-
# == Finding records by a value object
-
#
-
# Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
-
# by specifying an instance of the value object in the conditions hash. The following example
-
# finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
-
#
-
# Customer.where(balance: Money.new(20, "USD"))
-
#
-
2
module ClassMethods
-
# Adds reader and writer methods for manipulating a value object:
-
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
-
#
-
# Options are:
-
# * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
-
# can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
-
# to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
-
# with this option.
-
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
-
# object. Each mapping is represented as an array where the first item is the name of the
-
# entity attribute and the second item is the name of the attribute in the value object. The
-
# order in which mappings are defined determines the order in which attributes are sent to the
-
# value class constructor.
-
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
-
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
-
# mapped attributes.
-
# This defaults to +false+.
-
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
-
# is called to initialize the value object. The constructor is passed all of the mapped attributes,
-
# in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
-
# to instantiate a <tt>:class_name</tt> object.
-
# The default is <tt>:new</tt>.
-
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
-
# or a Proc that is called when a new value is assigned to the value object. The converter is
-
# passed the single value that is used in the assignment and is only called if the new value is
-
# not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter
-
# can return nil to skip the assignment.
-
#
-
# Option examples:
-
# composed_of :temperature, mapping: %w(reading celsius)
-
# composed_of :balance, class_name: "Money", mapping: %w(balance amount),
-
# converter: Proc.new { |balance| balance.to_money }
-
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
-
# composed_of :gps_location
-
# composed_of :gps_location, allow_nil: true
-
# composed_of :ip_address,
-
# class_name: 'IPAddr',
-
# mapping: %w(ip to_i),
-
# constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
-
# converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
-
#
-
2
def composed_of(part_id, options = {})
-
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
-
-
name = part_id.id2name
-
class_name = options[:class_name] || name.camelize
-
mapping = options[:mapping] || [ name, name ]
-
mapping = [ mapping ] unless mapping.first.is_a?(Array)
-
allow_nil = options[:allow_nil] || false
-
constructor = options[:constructor] || :new
-
converter = options[:converter]
-
-
reader_method(name, class_name, mapping, allow_nil, constructor)
-
writer_method(name, class_name, mapping, allow_nil, converter)
-
-
reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
-
Reflection.add_aggregate_reflection self, part_id, reflection
-
end
-
-
2
private
-
2
def reader_method(name, class_name, mapping, allow_nil, constructor)
-
define_method(name) do
-
if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? })
-
attrs = mapping.collect {|key, _| _read_attribute(key)}
-
object = constructor.respond_to?(:call) ?
-
constructor.call(*attrs) :
-
class_name.constantize.send(constructor, *attrs)
-
@aggregation_cache[name] = object
-
end
-
@aggregation_cache[name]
-
end
-
end
-
-
2
def writer_method(name, class_name, mapping, allow_nil, converter)
-
define_method("#{name}=") do |part|
-
klass = class_name.constantize
-
if part.is_a?(Hash)
-
part = klass.new(*part.values)
-
end
-
-
unless part.is_a?(klass) || converter.nil? || part.nil?
-
part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
-
end
-
-
if part.nil? && allow_nil
-
mapping.each { |key, _| self[key] = nil }
-
@aggregation_cache[name] = nil
-
else
-
mapping.each { |key, value| self[key] = part.send(value) }
-
@aggregation_cache[name] = part.freeze
-
end
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
class AssociationRelation < Relation
-
2
def initialize(klass, table, association)
-
11
super(klass, table)
-
11
@association = association
-
end
-
-
2
def proxy_association
-
@association
-
end
-
-
2
def ==(other)
-
other == to_a
-
end
-
-
2
def build(*args, &block)
-
scoping { @association.build(*args, &block) }
-
end
-
2
alias new build
-
-
2
def create(*args, &block)
-
scoping { @association.create(*args, &block) }
-
end
-
-
2
def create!(*args, &block)
-
scoping { @association.create!(*args, &block) }
-
end
-
-
2
private
-
-
2
def exec_queries
-
super.each { |r| @association.set_inverse_instance r }
-
end
-
end
-
end
-
2
require 'active_support/core_ext/enumerable'
-
2
require 'active_support/core_ext/string/conversions'
-
2
require 'active_support/core_ext/module/remove_method'
-
2
require 'active_record/errors'
-
-
2
module ActiveRecord
-
2
class AssociationNotFoundError < ConfigurationError #:nodoc:
-
2
def initialize(record, association_name)
-
super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
-
end
-
end
-
-
2
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
-
2
def initialize(reflection, associated_class = nil)
-
super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
-
end
-
end
-
-
2
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
-
2
def initialize(owner_class_name, reflection)
-
super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
-
end
-
end
-
-
2
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
-
2
def initialize(owner_class_name, reflection, source_reflection)
-
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.")
-
end
-
end
-
-
2
class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
-
2
def initialize(owner_class_name, reflection)
-
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
-
end
-
end
-
-
2
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
-
2
def initialize(owner_class_name, reflection, source_reflection)
-
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
-
end
-
end
-
-
2
class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
-
2
def initialize(owner_class_name, reflection, through_reflection)
-
super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
-
end
-
end
-
-
2
class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
-
2
def initialize(owner_class_name, reflection)
-
super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
-
end
-
end
-
-
2
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
-
2
def initialize(reflection)
-
through_reflection = reflection.through_reflection
-
source_reflection_names = reflection.source_reflection_names
-
source_associations = reflection.through_reflection.klass._reflections.keys
-
super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
-
end
-
end
-
-
2
class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
-
2
def initialize(owner, reflection)
-
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
-
end
-
end
-
-
2
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
-
2
def initialize(owner, reflection)
-
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
-
end
-
end
-
-
2
class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
-
2
def initialize(owner, reflection)
-
super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
-
end
-
end
-
-
2
class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
-
2
def initialize(owner, reflection)
-
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
-
end
-
end
-
-
2
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
-
2
def initialize(reflection)
-
super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
-
end
-
end
-
-
2
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
-
2
def initialize(reflection)
-
super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
-
end
-
end
-
-
# This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
-
# (has_many, has_one) when there is at least 1 child associated instance.
-
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
-
2
class DeleteRestrictionError < ActiveRecordError #:nodoc:
-
2
def initialize(name)
-
super("Cannot delete record because of dependent #{name}")
-
end
-
end
-
-
# See ActiveRecord::Associations::ClassMethods for documentation.
-
2
module Associations # :nodoc:
-
2
extend ActiveSupport::Autoload
-
2
extend ActiveSupport::Concern
-
-
# These classes will be loaded when associations are created.
-
# So there is no need to eager load them.
-
2
autoload :Association, 'active_record/associations/association'
-
2
autoload :SingularAssociation, 'active_record/associations/singular_association'
-
2
autoload :CollectionAssociation, 'active_record/associations/collection_association'
-
2
autoload :ForeignAssociation, 'active_record/associations/foreign_association'
-
2
autoload :CollectionProxy, 'active_record/associations/collection_proxy'
-
-
2
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
-
2
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
-
2
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
-
2
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
-
2
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
-
2
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
-
2
autoload :ThroughAssociation, 'active_record/associations/through_association'
-
-
2
module Builder #:nodoc:
-
2
autoload :Association, 'active_record/associations/builder/association'
-
2
autoload :SingularAssociation, 'active_record/associations/builder/singular_association'
-
2
autoload :CollectionAssociation, 'active_record/associations/builder/collection_association'
-
-
2
autoload :BelongsTo, 'active_record/associations/builder/belongs_to'
-
2
autoload :HasOne, 'active_record/associations/builder/has_one'
-
2
autoload :HasMany, 'active_record/associations/builder/has_many'
-
2
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
-
end
-
-
2
eager_autoload do
-
2
autoload :Preloader, 'active_record/associations/preloader'
-
2
autoload :JoinDependency, 'active_record/associations/join_dependency'
-
2
autoload :AssociationScope, 'active_record/associations/association_scope'
-
2
autoload :AliasTracker, 'active_record/associations/alias_tracker'
-
end
-
-
# Clears out the association cache.
-
2
def clear_association_cache #:nodoc:
-
@association_cache.clear if persisted?
-
end
-
-
# :nodoc:
-
2
attr_reader :association_cache
-
-
# Returns the association instance for the given name, instantiating it if it doesn't already exist
-
2
def association(name) #:nodoc:
-
7
association = association_instance_get(name)
-
-
7
if association.nil?
-
7
raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name)
-
7
association = reflection.association_class.new(self, reflection)
-
7
association_instance_set(name, association)
-
end
-
-
7
association
-
end
-
-
2
private
-
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
-
2
def association_instance_get(name)
-
63
@association_cache[name]
-
end
-
-
# Set the specified association instance.
-
2
def association_instance_set(name, association)
-
7
@association_cache[name] = association
-
end
-
-
# \Associations are a set of macro-like class methods for tying objects together through
-
# foreign keys. They express relationships like "Project has one Project Manager"
-
# or "Project belongs to a Portfolio". Each macro adds a number of methods to the
-
# class which are specialized according to the collection or association symbol and the
-
# options hash. It works much the same way as Ruby's own <tt>attr*</tt>
-
# methods.
-
#
-
# class Project < ActiveRecord::Base
-
# belongs_to :portfolio
-
# has_one :project_manager
-
# has_many :milestones
-
# has_and_belongs_to_many :categories
-
# end
-
#
-
# The project class now has the following methods (and more) to ease the traversal and
-
# manipulation of its relationships:
-
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
-
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
-
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
-
# <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt>
-
# <tt>Project#milestones.build, Project#milestones.create</tt>
-
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
-
# <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt>
-
#
-
# === A word of warning
-
#
-
# Don't create associations that have the same name as instance methods of
-
# <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to
-
# its model, it will override the inherited method and break things.
-
# For instance, +attributes+ and +connection+ would be bad choices for association names.
-
#
-
# == Auto-generated methods
-
# See also Instance Public methods below for more details.
-
#
-
# === Singular associations (one-to-one)
-
# | | belongs_to |
-
# generated methods | belongs_to | :polymorphic | has_one
-
# ----------------------------------+------------+--------------+---------
-
# other(force_reload=false) | X | X | X
-
# other=(other) | X | X | X
-
# build_other(attributes={}) | X | | X
-
# create_other(attributes={}) | X | | X
-
# create_other!(attributes={}) | X | | X
-
#
-
# ===Collection associations (one-to-many / many-to-many)
-
# | | | has_many
-
# generated methods | habtm | has_many | :through
-
# ----------------------------------+-------+----------+----------
-
# others(force_reload=false) | X | X | X
-
# others=(other,other,...) | X | X | X
-
# other_ids | X | X | X
-
# other_ids=(id,id,...) | X | X | X
-
# others<< | X | X | X
-
# others.push | X | X | X
-
# others.concat | X | X | X
-
# others.build(attributes={}) | X | X | X
-
# others.create(attributes={}) | X | X | X
-
# others.create!(attributes={}) | X | X | X
-
# others.size | X | X | X
-
# others.length | X | X | X
-
# others.count | X | X | X
-
# others.sum(*args) | X | X | X
-
# others.empty? | X | X | X
-
# others.clear | X | X | X
-
# others.delete(other,other,...) | X | X | X
-
# others.delete_all | X | X | X
-
# others.destroy(other,other,...) | X | X | X
-
# others.destroy_all | X | X | X
-
# others.find(*args) | X | X | X
-
# others.exists? | X | X | X
-
# others.distinct | X | X | X
-
# others.uniq | X | X | X
-
# others.reset | X | X | X
-
#
-
# === Overriding generated methods
-
#
-
# Association methods are generated in a module that is included into the model class,
-
# which allows you to easily override with your own methods and call the original
-
# generated method with +super+. For example:
-
#
-
# class Car < ActiveRecord::Base
-
# belongs_to :owner
-
# belongs_to :old_owner
-
# def owner=(new_owner)
-
# self.old_owner = self.owner
-
# super
-
# end
-
# end
-
#
-
# If your model class is <tt>Project</tt>, the module is
-
# named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
-
# included in the model class immediately after the (anonymous) generated attributes methods
-
# module, meaning an association will override the methods for an attribute with the same name.
-
#
-
# == Cardinality and associations
-
#
-
# Active Record associations can be used to describe one-to-one, one-to-many and many-to-many
-
# relationships between models. Each model uses an association to describe its role in
-
# the relation. The +belongs_to+ association is always used in the model that has
-
# the foreign key.
-
#
-
# === One-to-one
-
#
-
# Use +has_one+ in the base, and +belongs_to+ in the associated model.
-
#
-
# class Employee < ActiveRecord::Base
-
# has_one :office
-
# end
-
# class Office < ActiveRecord::Base
-
# belongs_to :employee # foreign key - employee_id
-
# end
-
#
-
# === One-to-many
-
#
-
# Use +has_many+ in the base, and +belongs_to+ in the associated model.
-
#
-
# class Manager < ActiveRecord::Base
-
# has_many :employees
-
# end
-
# class Employee < ActiveRecord::Base
-
# belongs_to :manager # foreign key - manager_id
-
# end
-
#
-
# === Many-to-many
-
#
-
# There are two ways to build a many-to-many relationship.
-
#
-
# The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so
-
# there are two stages of associations.
-
#
-
# class Assignment < ActiveRecord::Base
-
# belongs_to :programmer # foreign key - programmer_id
-
# belongs_to :project # foreign key - project_id
-
# end
-
# class Programmer < ActiveRecord::Base
-
# has_many :assignments
-
# has_many :projects, through: :assignments
-
# end
-
# class Project < ActiveRecord::Base
-
# has_many :assignments
-
# has_many :programmers, through: :assignments
-
# end
-
#
-
# For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table
-
# that has no corresponding model or primary key.
-
#
-
# class Programmer < ActiveRecord::Base
-
# has_and_belongs_to_many :projects # foreign keys in the join table
-
# end
-
# class Project < ActiveRecord::Base
-
# has_and_belongs_to_many :programmers # foreign keys in the join table
-
# end
-
#
-
# Choosing which way to build a many-to-many relationship is not always simple.
-
# If you need to work with the relationship model as its own entity,
-
# use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when
-
# you never work directly with the relationship itself.
-
#
-
# == Is it a +belongs_to+ or +has_one+ association?
-
#
-
# Both express a 1-1 relationship. The difference is mostly where to place the foreign
-
# key, which goes on the table for the class declaring the +belongs_to+ relationship.
-
#
-
# class User < ActiveRecord::Base
-
# # I reference an account.
-
# belongs_to :account
-
# end
-
#
-
# class Account < ActiveRecord::Base
-
# # One user references me.
-
# has_one :user
-
# end
-
#
-
# The tables for these classes could look something like:
-
#
-
# CREATE TABLE users (
-
# id int(11) NOT NULL auto_increment,
-
# account_id int(11) default NULL,
-
# name varchar default NULL,
-
# PRIMARY KEY (id)
-
# )
-
#
-
# CREATE TABLE accounts (
-
# id int(11) NOT NULL auto_increment,
-
# name varchar default NULL,
-
# PRIMARY KEY (id)
-
# )
-
#
-
# == Unsaved objects and associations
-
#
-
# You can manipulate objects and associations before they are saved to the database, but
-
# there is some special behavior you should be aware of, mostly involving the saving of
-
# associated objects.
-
#
-
# You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
-
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
-
# to +true+ will _always_ save the members, whereas setting it to +false+ will
-
# _never_ save the members. More details about <tt>:autosave</tt> option is available at
-
# AutosaveAssociation.
-
#
-
# === One-to-one associations
-
#
-
# * Assigning an object to a +has_one+ association automatically saves that object and
-
# the object being replaced (if there is one), in order to update their foreign
-
# keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
-
# * If either of these saves fail (due to one of the objects being invalid), an
-
# <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
-
# cancelled.
-
# * If you wish to assign an object to a +has_one+ association without saving it,
-
# use the <tt>build_association</tt> method (documented below). The object being
-
# replaced will still be saved to update its foreign key.
-
# * Assigning an object to a +belongs_to+ association does not save the object, since
-
# the foreign key field belongs on the parent. It does not save the parent either.
-
#
-
# === Collections
-
#
-
# * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically
-
# saves that object, except if the parent object (the owner of the collection) is not yet
-
# stored in the database.
-
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
-
# fails, then <tt>push</tt> returns +false+.
-
# * If saving fails while replacing the collection (via <tt>association=</tt>), an
-
# <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
-
# cancelled.
-
# * You can add an object to a collection without automatically saving it by using the
-
# <tt>collection.build</tt> method (documented below).
-
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
-
# saved when the parent is saved.
-
#
-
# == Customizing the query
-
#
-
# \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
-
# to customize them. For example, to add a condition:
-
#
-
# class Blog < ActiveRecord::Base
-
# has_many :published_posts, -> { where published: true }, class_name: 'Post'
-
# end
-
#
-
# Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods.
-
#
-
# === Accessing the owner object
-
#
-
# Sometimes it is useful to have access to the owner object when building the query. The owner
-
# is passed as a parameter to the block. For example, the following association would find all
-
# events that occur on the user's birthday:
-
#
-
# class User < ActiveRecord::Base
-
# has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
-
# end
-
#
-
# Note: Joining, eager loading and preloading of these associations is not fully possible.
-
# These operations happen before instance creation and the scope will be called with a +nil+ argument.
-
# This can lead to unexpected behavior and is deprecated.
-
#
-
# == Association callbacks
-
#
-
# Similar to the normal callbacks that hook into the life cycle of an Active Record object,
-
# you can also define callbacks that get triggered when you add an object to or remove an
-
# object from an association collection.
-
#
-
# class Project
-
# has_and_belongs_to_many :developers, after_add: :evaluate_velocity
-
#
-
# def evaluate_velocity(developer)
-
# ...
-
# end
-
# end
-
#
-
# It's possible to stack callbacks by passing them as an array. Example:
-
#
-
# class Project
-
# has_and_belongs_to_many :developers,
-
# after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
-
# end
-
#
-
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
-
#
-
# If any of the +before_add+ callbacks throw an exception, the object will not be
-
# added to the collection.
-
#
-
# Similarly, if any of the +before_remove+ callbacks throw an exception, the object
-
# will not be removed from the collection.
-
#
-
# == Association extensions
-
#
-
# The proxy objects that control the access to associations can be extended through anonymous
-
# modules. This is especially beneficial for adding new finders, creators, and other
-
# factory-type methods that are only used as part of this association.
-
#
-
# class Account < ActiveRecord::Base
-
# has_many :people do
-
# def find_or_create_by_name(name)
-
# first_name, last_name = name.split(" ", 2)
-
# find_or_create_by(first_name: first_name, last_name: last_name)
-
# end
-
# end
-
# end
-
#
-
# person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
-
# person.first_name # => "David"
-
# person.last_name # => "Heinemeier Hansson"
-
#
-
# If you need to share the same extensions between many associations, you can use a named
-
# extension module.
-
#
-
# module FindOrCreateByNameExtension
-
# def find_or_create_by_name(name)
-
# first_name, last_name = name.split(" ", 2)
-
# find_or_create_by(first_name: first_name, last_name: last_name)
-
# end
-
# end
-
#
-
# class Account < ActiveRecord::Base
-
# has_many :people, -> { extending FindOrCreateByNameExtension }
-
# end
-
#
-
# class Company < ActiveRecord::Base
-
# has_many :people, -> { extending FindOrCreateByNameExtension }
-
# end
-
#
-
# Some extensions can only be made to work with knowledge of the association's internals.
-
# Extensions can access relevant state using the following methods (where +items+ is the
-
# name of the association):
-
#
-
# * <tt>record.association(:items).owner</tt> - Returns the object the association is part of.
-
# * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association.
-
# * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or
-
# the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
-
#
-
# However, inside the actual extension code, you will not have access to the <tt>record</tt> as
-
# above. In this case, you can access <tt>proxy_association</tt>. For example,
-
# <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return
-
# the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside
-
# association extensions.
-
#
-
# == Association Join Models
-
#
-
# Has Many associations can be configured with the <tt>:through</tt> option to use an
-
# explicit join model to retrieve the data. This operates similarly to a
-
# +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
-
# callbacks, and extra attributes on the join model. Consider the following schema:
-
#
-
# class Author < ActiveRecord::Base
-
# has_many :authorships
-
# has_many :books, through: :authorships
-
# end
-
#
-
# class Authorship < ActiveRecord::Base
-
# belongs_to :author
-
# belongs_to :book
-
# end
-
#
-
# @author = Author.first
-
# @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
-
# @author.books # selects all books by using the Authorship join model
-
#
-
# You can also go through a +has_many+ association on the join model:
-
#
-
# class Firm < ActiveRecord::Base
-
# has_many :clients
-
# has_many :invoices, through: :clients
-
# end
-
#
-
# class Client < ActiveRecord::Base
-
# belongs_to :firm
-
# has_many :invoices
-
# end
-
#
-
# class Invoice < ActiveRecord::Base
-
# belongs_to :client
-
# end
-
#
-
# @firm = Firm.first
-
# @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
-
# @firm.invoices # selects all invoices by going through the Client join model
-
#
-
# Similarly you can go through a +has_one+ association on the join model:
-
#
-
# class Group < ActiveRecord::Base
-
# has_many :users
-
# has_many :avatars, through: :users
-
# end
-
#
-
# class User < ActiveRecord::Base
-
# belongs_to :group
-
# has_one :avatar
-
# end
-
#
-
# class Avatar < ActiveRecord::Base
-
# belongs_to :user
-
# end
-
#
-
# @group = Group.first
-
# @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
-
# @group.avatars # selects all avatars by going through the User join model.
-
#
-
# An important caveat with going through +has_one+ or +has_many+ associations on the
-
# join model is that these associations are *read-only*. For example, the following
-
# would not work following the previous example:
-
#
-
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
-
# @group.avatars.delete(@group.avatars.last) # so would this
-
#
-
# == Setting Inverses
-
#
-
# If you are using a +belongs_to+ on the join model, it is a good idea to set the
-
# <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
-
# works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
-
#
-
# @post = Post.first
-
# @tag = @post.tags.build name: "ruby"
-
# @tag.save
-
#
-
# The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
-
# <tt>:inverse_of</tt> is set:
-
#
-
# class Taggable < ActiveRecord::Base
-
# belongs_to :post
-
# belongs_to :tag, inverse_of: :taggings
-
# end
-
#
-
# If you do not set the <tt>:inverse_of</tt> record, the association will
-
# do its best to match itself up with the correct inverse. Automatic
-
# inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
-
# <tt>belongs_to</tt> associations.
-
#
-
# Extra options on the associations, as defined in the
-
# <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
-
# also prevent the association's inverse from being found automatically.
-
#
-
# The automatic guessing of the inverse association uses a heuristic based
-
# on the name of the class, so it may not work for all associations,
-
# especially the ones with non-standard names.
-
#
-
# You can turn off the automatic detection of inverse associations by setting
-
# the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
-
#
-
# class Taggable < ActiveRecord::Base
-
# belongs_to :tag, inverse_of: false
-
# end
-
#
-
# == Nested \Associations
-
#
-
# You can actually specify *any* association with the <tt>:through</tt> option, including an
-
# association which has a <tt>:through</tt> option itself. For example:
-
#
-
# class Author < ActiveRecord::Base
-
# has_many :posts
-
# has_many :comments, through: :posts
-
# has_many :commenters, through: :comments
-
# end
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :comments
-
# end
-
#
-
# class Comment < ActiveRecord::Base
-
# belongs_to :commenter
-
# end
-
#
-
# @author = Author.first
-
# @author.commenters # => People who commented on posts written by the author
-
#
-
# An equivalent way of setting up this association this would be:
-
#
-
# class Author < ActiveRecord::Base
-
# has_many :posts
-
# has_many :commenters, through: :posts
-
# end
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :comments
-
# has_many :commenters, through: :comments
-
# end
-
#
-
# class Comment < ActiveRecord::Base
-
# belongs_to :commenter
-
# end
-
#
-
# When using a nested association, you will not be able to modify the association because there
-
# is not enough information to know what modification to make. For example, if you tried to
-
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
-
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
-
#
-
# == Polymorphic \Associations
-
#
-
# Polymorphic associations on models are not restricted on what types of models they
-
# can be associated with. Rather, they specify an interface that a +has_many+ association
-
# must adhere to.
-
#
-
# class Asset < ActiveRecord::Base
-
# belongs_to :attachable, polymorphic: true
-
# end
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use.
-
# end
-
#
-
# @asset.attachable = @post
-
#
-
# This works by using a type column in addition to a foreign key to specify the associated
-
# record. In the Asset example, you'd need an +attachable_id+ integer column and an
-
# +attachable_type+ string column.
-
#
-
# Using polymorphic associations in combination with single table inheritance (STI) is
-
# a little tricky. In order for the associations to work as expected, ensure that you
-
# store the base model for the STI models in the type column of the polymorphic
-
# association. To continue with the asset example above, suppose there are guest posts
-
# and member posts that use the posts table for STI. In this case, there must be a +type+
-
# column in the posts table.
-
#
-
# Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+.
-
# The +class_name+ of the +attachable+ is passed as a String.
-
#
-
# class Asset < ActiveRecord::Base
-
# belongs_to :attachable, polymorphic: true
-
#
-
# def attachable_type=(class_name)
-
# super(class_name.constantize.base_class.to_s)
-
# end
-
# end
-
#
-
# class Post < ActiveRecord::Base
-
# # because we store "Post" in attachable_type now dependent: :destroy will work
-
# has_many :assets, as: :attachable, dependent: :destroy
-
# end
-
#
-
# class GuestPost < Post
-
# end
-
#
-
# class MemberPost < Post
-
# end
-
#
-
# == Caching
-
#
-
# All of the methods are built on a simple caching principle that will keep the result
-
# of the last query around unless specifically instructed not to. The cache is even
-
# shared across methods to make it even cheaper to use the macro-added methods without
-
# worrying too much about performance at the first go.
-
#
-
# project.milestones # fetches milestones from the database
-
# project.milestones.size # uses the milestone cache
-
# project.milestones.empty? # uses the milestone cache
-
# project.milestones(true).size # fetches milestones from the database
-
# project.milestones # uses the milestone cache
-
#
-
# == Eager loading of associations
-
#
-
# Eager loading is a way to find objects of a certain class and a number of named associations.
-
# It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
-
# posts that each need to display their author triggers 101 database queries. Through the
-
# use of eager loading, the number of queries will be reduced from 101 to 2.
-
#
-
# class Post < ActiveRecord::Base
-
# belongs_to :author
-
# has_many :comments
-
# end
-
#
-
# Consider the following loop using the class above:
-
#
-
# Post.all.each do |post|
-
# puts "Post: " + post.title
-
# puts "Written by: " + post.author.name
-
# puts "Last comment on: " + post.comments.first.created_on
-
# end
-
#
-
# To iterate over these one hundred posts, we'll generate 201 database queries. Let's
-
# first just optimize it for retrieving the author:
-
#
-
# Post.includes(:author).each do |post|
-
#
-
# This references the name of the +belongs_to+ association that also used the <tt>:author</tt>
-
# symbol. After loading the posts, find will collect the +author_id+ from each one and load
-
# all the referenced authors with one query. Doing so will cut down the number of queries
-
# from 201 to 102.
-
#
-
# We can improve upon the situation further by referencing both associations in the finder with:
-
#
-
# Post.includes(:author, :comments).each do |post|
-
#
-
# This will load all comments with a single query. This reduces the total number of queries
-
# to 3. In general, the number of queries will be 1 plus the number of associations
-
# named (except if some of the associations are polymorphic +belongs_to+ - see below).
-
#
-
# To include a deep hierarchy of associations, use a hash:
-
#
-
# Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
-
#
-
# The above code will load all the comments and all of their associated
-
# authors and gravatars. You can mix and match any combination of symbols,
-
# arrays, and hashes to retrieve the associations you want to load.
-
#
-
# All of this power shouldn't fool you into thinking that you can pull out huge amounts
-
# of data with no performance penalty just because you've reduced the number of queries.
-
# The database still needs to send all the data to Active Record and it still needs to
-
# be processed. So it's no catch-all for performance problems, but it's a great way to
-
# cut down on the number of queries in a situation as the one described above.
-
#
-
# Since only one table is loaded at a time, conditions or orders cannot reference tables
-
# other than the main one. If this is the case, Active Record falls back to the previously
-
# used LEFT OUTER JOIN based strategy. For example:
-
#
-
# Post.includes([:author, :comments]).where(['comments.approved = ?', true])
-
#
-
# This will result in a single SQL query with joins along the lines of:
-
# <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
-
# <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions
-
# like this can have unintended consequences.
-
# In the above example posts with no approved comments are not returned at all, because
-
# the conditions apply to the SQL statement as a whole and not just to the association.
-
#
-
# You must disambiguate column references for this fallback to happen, for example
-
# <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
-
#
-
# If you want to load all posts (including posts with no approved comments) then write
-
# your own LEFT OUTER JOIN query using ON
-
#
-
# Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
-
#
-
# In this case it is usually more natural to include an association which has conditions defined on it:
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
-
# end
-
#
-
# Post.includes(:approved_comments)
-
#
-
# This will load posts and eager load the +approved_comments+ association, which contains
-
# only those comments that have been approved.
-
#
-
# If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored,
-
# returning all the associated objects:
-
#
-
# class Picture < ActiveRecord::Base
-
# has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
-
# end
-
#
-
# Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
-
#
-
# Eager loading is supported with polymorphic associations.
-
#
-
# class Address < ActiveRecord::Base
-
# belongs_to :addressable, polymorphic: true
-
# end
-
#
-
# A call that tries to eager load the addressable model
-
#
-
# Address.includes(:addressable)
-
#
-
# This will execute one query to load the addresses and load the addressables with one
-
# query per addressable type.
-
# For example if all the addressables are either of class Person or Company then a total
-
# of 3 queries will be executed. The list of addressable types to load is determined on
-
# the back of the addresses loaded. This is not supported if Active Record has to fallback
-
# to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>.
-
# The reason is that the parent model's type is a column value so its corresponding table
-
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
-
#
-
# == Table Aliasing
-
#
-
# Active Record uses table aliasing in the case that a table is referenced multiple times
-
# in a join. If a table is referenced only once, the standard table name is used. The
-
# second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>.
-
# Indexes are appended for any more successive uses of the table name.
-
#
-
# Post.joins(:comments)
-
# # => SELECT ... FROM posts INNER JOIN comments ON ...
-
# Post.joins(:special_comments) # STI
-
# # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
-
# Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
-
# # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
-
#
-
# Acts as tree example:
-
#
-
# TreeMixin.joins(:children)
-
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
-
# TreeMixin.joins(children: :parent)
-
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
-
# INNER JOIN parents_mixins ...
-
# TreeMixin.joins(children: {parent: :children})
-
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
-
# INNER JOIN parents_mixins ...
-
# INNER JOIN mixins childrens_mixins_2
-
#
-
# Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
-
#
-
# Post.joins(:categories)
-
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
-
# Post.joins(categories: :posts)
-
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
-
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
-
# Post.joins(categories: {posts: :categories})
-
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
-
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
-
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
-
#
-
# If you wish to specify your own custom joins using <tt>joins</tt> method, those table
-
# names will take precedence over the eager associations:
-
#
-
# Post.joins(:comments).joins("inner join comments ...")
-
# # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
-
# Post.joins(:comments, :special_comments).joins("inner join comments ...")
-
# # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
-
# INNER JOIN comments special_comments_posts ...
-
# INNER JOIN comments ...
-
#
-
# Table aliases are automatically truncated according to the maximum length of table identifiers
-
# according to the specific database.
-
#
-
# == Modules
-
#
-
# By default, associations will look for objects within the current module scope. Consider:
-
#
-
# module MyApplication
-
# module Business
-
# class Firm < ActiveRecord::Base
-
# has_many :clients
-
# end
-
#
-
# class Client < ActiveRecord::Base; end
-
# end
-
# end
-
#
-
# When <tt>Firm#clients</tt> is called, it will in turn call
-
# <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
-
# If you want to associate with a class in another module scope, this can be done by
-
# specifying the complete class name.
-
#
-
# module MyApplication
-
# module Business
-
# class Firm < ActiveRecord::Base; end
-
# end
-
#
-
# module Billing
-
# class Account < ActiveRecord::Base
-
# belongs_to :firm, class_name: "MyApplication::Business::Firm"
-
# end
-
# end
-
# end
-
#
-
# == Bi-directional associations
-
#
-
# When you specify an association there is usually an association on the associated model
-
# that specifies the same relationship in reverse. For example, with the following models:
-
#
-
# class Dungeon < ActiveRecord::Base
-
# has_many :traps
-
# has_one :evil_wizard
-
# end
-
#
-
# class Trap < ActiveRecord::Base
-
# belongs_to :dungeon
-
# end
-
#
-
# class EvilWizard < ActiveRecord::Base
-
# belongs_to :dungeon
-
# end
-
#
-
# The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
-
# the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
-
# is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
-
# Active Record doesn't know anything about these inverse relationships and so no object
-
# loading optimization is possible. For example:
-
#
-
# d = Dungeon.first
-
# t = d.traps.first
-
# d.level == t.dungeon.level # => true
-
# d.level = 10
-
# d.level == t.dungeon.level # => false
-
#
-
# The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to
-
# the same object data from the database, but are actually different in-memory copies
-
# of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell
-
# Active Record about inverse relationships and it will optimise object loading. For
-
# example, if we changed our model definitions to:
-
#
-
# class Dungeon < ActiveRecord::Base
-
# has_many :traps, inverse_of: :dungeon
-
# has_one :evil_wizard, inverse_of: :dungeon
-
# end
-
#
-
# class Trap < ActiveRecord::Base
-
# belongs_to :dungeon, inverse_of: :traps
-
# end
-
#
-
# class EvilWizard < ActiveRecord::Base
-
# belongs_to :dungeon, inverse_of: :evil_wizard
-
# end
-
#
-
# Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same
-
# in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+.
-
#
-
# There are limitations to <tt>:inverse_of</tt> support:
-
#
-
# * does not work with <tt>:through</tt> associations.
-
# * does not work with <tt>:polymorphic</tt> associations.
-
# * for +belongs_to+ associations +has_many+ inverse associations are ignored.
-
#
-
# == Deleting from associations
-
#
-
# === Dependent associations
-
#
-
# +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option.
-
# This allows you to specify that associated records should be deleted when the owner is
-
# deleted.
-
#
-
# For example:
-
#
-
# class Author
-
# has_many :posts, dependent: :destroy
-
# end
-
# Author.find(1).destroy # => Will destroy all of the author's posts, too
-
#
-
# The <tt>:dependent</tt> option can have different values which specify how the deletion
-
# is done. For more information, see the documentation for this option on the different
-
# specific association types. When no option is given, the behavior is to do nothing
-
# with the associated records when destroying a record.
-
#
-
# Note that <tt>:dependent</tt> is implemented using Rails' callback
-
# system, which works by processing callbacks in order. Therefore, other
-
# callbacks declared either before or after the <tt>:dependent</tt> option
-
# can affect what it does.
-
#
-
# === Delete or destroy?
-
#
-
# +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
-
# <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
-
#
-
# For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
-
# cause the records in the join table to be removed.
-
#
-
# For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
-
# record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
-
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
-
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
-
# The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for
-
# +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
-
# the join records, without running their callbacks).
-
#
-
# There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
-
# it returns the association rather than the records which have been deleted.
-
#
-
# === What gets deleted?
-
#
-
# There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>
-
# associations have records in join tables, as well as the associated records. So when we
-
# call one of these deletion methods, what exactly should be deleted?
-
#
-
# The answer is that it is assumed that deletion on an association is about removing the
-
# <i>link</i> between the owner and the associated object(s), rather than necessarily the
-
# associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
-
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
-
#
-
# This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
-
# you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
-
# to be removed from the database.
-
#
-
# However, there are examples where this strategy doesn't make sense. For example, suppose
-
# a person has many projects, and each project has many tasks. If we deleted one of a person's
-
# tasks, we would probably not want the project to be deleted. In this scenario, the delete method
-
# won't actually work: it can only be used if the association on the join model is a
-
# +belongs_to+. In other situations you are expected to perform operations directly on
-
# either the associated records or the <tt>:through</tt> association.
-
#
-
# With a regular +has_many+ there is no distinction between the "associated records"
-
# and the "link", so there is only one choice for what gets deleted.
-
#
-
# With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the
-
# associated records themselves, you can always do something along the lines of
-
# <tt>person.tasks.each(&:destroy)</tt>.
-
#
-
# == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
-
#
-
# If you attempt to assign an object to an association that doesn't match the inferred
-
# or specified <tt>:class_name</tt>, you'll get an <tt>ActiveRecord::AssociationTypeMismatch</tt>.
-
#
-
# == Options
-
#
-
# All of the association macros can be specialized through options. This makes cases
-
# more complex than the simple and guessable ones possible.
-
2
module ClassMethods
-
# Specifies a one-to-many association. The following methods for retrieval and query of
-
# collections of associated objects will be added:
-
#
-
# +collection+ is a placeholder for the symbol passed as the +name+ argument, so
-
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
-
#
-
# [collection(force_reload = false)]
-
# Returns an array of all the associated objects.
-
# An empty array is returned if none are found.
-
# [collection<<(object, ...)]
-
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
-
# Note that this operation instantly fires update SQL without waiting for the save or update call on the
-
# parent object, unless the parent object is a new record.
-
# [collection.delete(object, ...)]
-
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
-
# Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
-
# and deleted if they're associated with <tt>dependent: :delete_all</tt>.
-
#
-
# If the <tt>:through</tt> option is used, then the join records are deleted (rather than
-
# nullified) by default, but you can specify <tt>dependent: :destroy</tt> or
-
# <tt>dependent: :nullify</tt> to override this.
-
# [collection.destroy(object, ...)]
-
# Removes one or more objects from the collection by running <tt>destroy</tt> on
-
# each record, regardless of any dependent option, ensuring callbacks are run.
-
#
-
# If the <tt>:through</tt> option is used, then the join records are destroyed
-
# instead, not the objects themselves.
-
# [collection=objects]
-
# Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
-
# option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
-
# direct.
-
# [collection_singular_ids]
-
# Returns an array of the associated objects' ids
-
# [collection_singular_ids=ids]
-
# Replace the collection with the objects identified by the primary keys in +ids+. This
-
# method loads the models and calls <tt>collection=</tt>. See above.
-
# [collection.clear]
-
# Removes every object from the collection. This destroys the associated objects if they
-
# are associated with <tt>dependent: :destroy</tt>, deletes them directly from the
-
# database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
-
# If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models.
-
# Join models are directly deleted.
-
# [collection.empty?]
-
# Returns +true+ if there are no associated objects.
-
# [collection.size]
-
# Returns the number of associated objects.
-
# [collection.find(...)]
-
# Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>.
-
# [collection.exists?(...)]
-
# Checks whether an associated object with the given conditions exists.
-
# Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
-
# [collection.build(attributes = {}, ...)]
-
# Returns one or more new objects of the collection type that have been instantiated
-
# with +attributes+ and linked to this object through a foreign key, but have not yet
-
# been saved.
-
# [collection.create(attributes = {})]
-
# Returns a new object of the collection type that has been instantiated
-
# with +attributes+, linked to this object through a foreign key, and that has already
-
# been saved (if it passed the validation). *Note*: This only works if the base model
-
# already exists in the DB, not if it is a new (unsaved) record!
-
# [collection.create!(attributes = {})]
-
# Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
-
# if the record is invalid.
-
#
-
# === Example
-
#
-
# A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
-
# * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
-
# * <tt>Firm#clients<<</tt>
-
# * <tt>Firm#clients.delete</tt>
-
# * <tt>Firm#clients.destroy</tt>
-
# * <tt>Firm#clients=</tt>
-
# * <tt>Firm#client_ids</tt>
-
# * <tt>Firm#client_ids=</tt>
-
# * <tt>Firm#clients.clear</tt>
-
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
-
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
-
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
-
# * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
-
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
-
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
-
# * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
-
# The declaration can also include an +options+ hash to specialize the behavior of the association.
-
#
-
# === Scopes
-
#
-
# You can pass a second argument +scope+ as a callable (i.e. proc or
-
# lambda) to retrieve a specific set of records or customize the generated
-
# query when you access the associated collection.
-
#
-
# Scope examples:
-
# has_many :comments, -> { where(author_id: 1) }
-
# has_many :employees, -> { joins(:address) }
-
# has_many :posts, ->(post) { where("max_post_length > ?", post.length) }
-
#
-
# === Extensions
-
#
-
# The +extension+ argument allows you to pass a block into a has_many
-
# association. This is useful for adding new finders, creators and other
-
# factory-type methods to be used as part of the association.
-
#
-
# Extension examples:
-
# has_many :employees do
-
# def find_or_create_by_name(name)
-
# first_name, last_name = name.split(" ", 2)
-
# find_or_create_by(first_name: first_name, last_name: last_name)
-
# end
-
# end
-
#
-
# === Options
-
# [:class_name]
-
# Specify the class name of the association. Use it only if that name can't be inferred
-
# from the association name. So <tt>has_many :products</tt> will by default be linked
-
# to the Product class, but if the real class name is SpecialProduct, you'll have to
-
# specify it with this option.
-
# [:foreign_key]
-
# Specify the foreign key used for the association. By default this is guessed to be the name
-
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+
-
# association will use "person_id" as the default <tt>:foreign_key</tt>.
-
# [:foreign_type]
-
# Specify the column used to store the associated object's type, if this is a polymorphic
-
# association. By default this is guessed to be the name of the polymorphic association
-
# specified on "as" option with a "_type" suffix. So a class that defines a
-
# <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the
-
# default <tt>:foreign_type</tt>.
-
# [:primary_key]
-
# Specify the name of the column to use as the primary key for the association. By default this is +id+.
-
# [:dependent]
-
# Controls what happens to the associated objects when
-
# their owner is destroyed. Note that these are implemented as
-
# callbacks, and Rails executes callbacks in order. Therefore, other
-
# similar callbacks may affect the <tt>:dependent</tt> behavior, and the
-
# <tt>:dependent</tt> behavior may affect other callbacks.
-
#
-
# * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
-
# * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
-
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
-
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records.
-
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
-
#
-
# If using with the <tt>:through</tt> option, the association on the join model must be
-
# a +belongs_to+, and the records which get deleted are the join records, rather than
-
# the associated records.
-
# [:counter_cache]
-
# This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
-
# when you customized the name of your <tt>:counter_cache</tt> on the <tt>belongs_to</tt> association.
-
# [:as]
-
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
-
# [:through]
-
# Specifies an association through which to perform the query. This can be any other type
-
# of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
-
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
-
# source reflection.
-
#
-
# If the association on the join model is a +belongs_to+, the collection can be modified
-
# and the records on the <tt>:through</tt> model will be automatically created and removed
-
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
-
# <tt>:through</tt> association directly.
-
#
-
# If you are going to modify the association (rather than just read from it), then it is
-
# a good idea to set the <tt>:inverse_of</tt> option on the source association on the
-
# join model. This allows associated records to be built which will automatically create
-
# the appropriate join model records when they are saved. (See the 'Association Join Models'
-
# section above.)
-
# [:source]
-
# Specifies the source association name used by <tt>has_many :through</tt> queries.
-
# Only use it if the name cannot be inferred from the association.
-
# <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
-
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
-
# [:source_type]
-
# Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
-
# association is a polymorphic +belongs_to+.
-
# [:validate]
-
# If +false+, don't validate the associated objects when saving the parent object. true by default.
-
# [:autosave]
-
# If true, always save the associated objects or destroy them if marked for destruction,
-
# when saving the parent object. If false, never save or destroy the associated objects.
-
# By default, only save associated objects that are new records. This option is implemented as a
-
# +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
-
# may need to be explicitly saved in any user-defined +before_save+ callbacks.
-
#
-
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
-
# [:inverse_of]
-
# Specifies the name of the <tt>belongs_to</tt> association on the associated object
-
# that is the inverse of this <tt>has_many</tt> association. Does not work in combination
-
# with <tt>:through</tt> or <tt>:as</tt> options.
-
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
-
#
-
# Option examples:
-
# has_many :comments, -> { order "posted_on" }
-
# has_many :comments, -> { includes :author }
-
# has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
-
# has_many :tracks, -> { order "position" }, dependent: :destroy
-
# has_many :comments, dependent: :nullify
-
# has_many :tags, as: :taggable
-
# has_many :reports, -> { readonly }
-
# has_many :subscribers, through: :subscriptions, source: :user
-
2
def has_many(name, scope = nil, options = {}, &extension)
-
16
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
-
16
Reflection.add_reflection self, name, reflection
-
end
-
-
# Specifies a one-to-one association with another class. This method should only be used
-
# if the other class contains the foreign key. If the current class contains the foreign key,
-
# then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
-
# on when to use +has_one+ and when to use +belongs_to+.
-
#
-
# The following methods for retrieval and query of a single associated object will be added:
-
#
-
# +association+ is a placeholder for the symbol passed as the +name+ argument, so
-
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
-
#
-
# [association(force_reload = false)]
-
# Returns the associated object. +nil+ is returned if none is found.
-
# [association=(associate)]
-
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
-
# and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
-
# associated object when assigning a new one, even if the new one isn't saved to database.
-
# [build_association(attributes = {})]
-
# Returns a new object of the associated type that has been instantiated
-
# with +attributes+ and linked to this object through a foreign key, but has not
-
# yet been saved.
-
# [create_association(attributes = {})]
-
# Returns a new object of the associated type that has been instantiated
-
# with +attributes+, linked to this object through a foreign key, and that
-
# has already been saved (if it passed the validation).
-
# [create_association!(attributes = {})]
-
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
-
# if the record is invalid.
-
#
-
# === Example
-
#
-
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
-
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
-
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
-
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
-
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
-
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
-
#
-
# === Scopes
-
#
-
# You can pass a second argument +scope+ as a callable (i.e. proc or
-
# lambda) to retrieve a specific record or customize the generated query
-
# when you access the associated object.
-
#
-
# Scope examples:
-
# has_one :author, -> { where(comment_id: 1) }
-
# has_one :employer, -> { joins(:company) }
-
# has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }
-
#
-
# === Options
-
#
-
# The declaration can also include an +options+ hash to specialize the behavior of the association.
-
#
-
# Options are:
-
# [:class_name]
-
# Specify the class name of the association. Use it only if that name can't be inferred
-
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
-
# if the real class name is Person, you'll have to specify it with this option.
-
# [:dependent]
-
# Controls what happens to the associated object when
-
# its owner is destroyed:
-
#
-
# * <tt>:destroy</tt> causes the associated object to also be destroyed
-
# * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
-
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
-
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
-
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
-
# [:foreign_key]
-
# Specify the foreign key used for the association. By default this is guessed to be the name
-
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association
-
# will use "person_id" as the default <tt>:foreign_key</tt>.
-
# [:foreign_type]
-
# Specify the column used to store the associated object's type, if this is a polymorphic
-
# association. By default this is guessed to be the name of the polymorphic association
-
# specified on "as" option with a "_type" suffix. So a class that defines a
-
# <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the
-
# default <tt>:foreign_type</tt>.
-
# [:primary_key]
-
# Specify the method that returns the primary key used for the association. By default this is +id+.
-
# [:as]
-
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
-
# [:through]
-
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
-
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
-
# source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt>
-
# or <tt>belongs_to</tt> association on the join model.
-
# [:source]
-
# Specifies the source association name used by <tt>has_one :through</tt> queries.
-
# Only use it if the name cannot be inferred from the association.
-
# <tt>has_one :favorite, through: :favorites</tt> will look for a
-
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
-
# [:source_type]
-
# Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
-
# association is a polymorphic +belongs_to+.
-
# [:validate]
-
# If +false+, don't validate the associated object when saving the parent object. +false+ by default.
-
# [:autosave]
-
# If true, always save the associated object or destroy it if marked for destruction,
-
# when saving the parent object. If false, never save or destroy the associated object.
-
# By default, only save the associated object if it's a new record.
-
#
-
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
-
# [:inverse_of]
-
# Specifies the name of the <tt>belongs_to</tt> association on the associated object
-
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
-
# with <tt>:through</tt> or <tt>:as</tt> options.
-
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
-
# [:required]
-
# When set to +true+, the association will also have its presence validated.
-
# This will validate the association itself, not the id. You can use
-
# +:inverse_of+ to avoid an extra query during validation.
-
#
-
# Option examples:
-
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
-
# has_one :credit_card, dependent: :nullify # updates the associated records foreign
-
# # key value to NULL rather than destroying it
-
# has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment"
-
# has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person"
-
# has_one :attachment, as: :attachable
-
# has_one :boss, -> { readonly }
-
# has_one :club, through: :membership
-
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
-
# has_one :credit_card, required: true
-
2
def has_one(name, scope = nil, options = {})
-
reflection = Builder::HasOne.build(self, name, scope, options)
-
Reflection.add_reflection self, name, reflection
-
end
-
-
# Specifies a one-to-one association with another class. This method should only be used
-
# if this class contains the foreign key. If the other class contains the foreign key,
-
# then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview
-
# on when to use +has_one+ and when to use +belongs_to+.
-
#
-
# Methods will be added for retrieval and query for a single associated object, for which
-
# this object holds an id:
-
#
-
# +association+ is a placeholder for the symbol passed as the +name+ argument, so
-
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
-
#
-
# [association(force_reload = false)]
-
# Returns the associated object. +nil+ is returned if none is found.
-
# [association=(associate)]
-
# Assigns the associate object, extracts the primary key, and sets it as the foreign key.
-
# [build_association(attributes = {})]
-
# Returns a new object of the associated type that has been instantiated
-
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
-
# [create_association(attributes = {})]
-
# Returns a new object of the associated type that has been instantiated
-
# with +attributes+, linked to this object through a foreign key, and that
-
# has already been saved (if it passed the validation).
-
# [create_association!(attributes = {})]
-
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
-
# if the record is invalid.
-
#
-
# === Example
-
#
-
# A Post class declares <tt>belongs_to :author</tt>, which will add:
-
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
-
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
-
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
-
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
-
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
-
# The declaration can also include an +options+ hash to specialize the behavior of the association.
-
#
-
# === Scopes
-
#
-
# You can pass a second argument +scope+ as a callable (i.e. proc or
-
# lambda) to retrieve a specific record or customize the generated query
-
# when you access the associated object.
-
#
-
# Scope examples:
-
# belongs_to :user, -> { where(id: 2) }
-
# belongs_to :user, -> { joins(:friends) }
-
# belongs_to :level, ->(level) { where("game_level > ?", level.current) }
-
#
-
# === Options
-
#
-
# [:class_name]
-
# Specify the class name of the association. Use it only if that name can't be inferred
-
# from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but
-
# if the real class name is Person, you'll have to specify it with this option.
-
# [:foreign_key]
-
# Specify the foreign key used for the association. By default this is guessed to be the name
-
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
-
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
-
# <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
-
# of "favorite_person_id".
-
# [:foreign_type]
-
# Specify the column used to store the associated object's type, if this is a polymorphic
-
# association. By default this is guessed to be the name of the association with a "_type"
-
# suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt>
-
# association will use "taggable_type" as the default <tt>:foreign_type</tt>.
-
# [:primary_key]
-
# Specify the method that returns the primary key of associated object used for the association.
-
# By default this is id.
-
# [:dependent]
-
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
-
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
-
# This option should not be specified when <tt>belongs_to</tt> is used in conjunction with
-
# a <tt>has_many</tt> relationship on another class because of the potential to leave
-
# orphaned records behind.
-
# [:counter_cache]
-
# Caches the number of belonging objects on the associate class through the use of +increment_counter+
-
# and +decrement_counter+. The counter cache is incremented when an object of this
-
# class is created and decremented when it's destroyed. This requires that a column
-
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
-
# is used on the associate class (such as a Post class) - that is the migration for
-
# <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
-
# return the count cached, see note below). You can also specify a custom counter
-
# cache column by providing a column name instead of a +true+/+false+ value to this
-
# option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
-
# Note: Specifying a counter cache will add it to that model's list of readonly attributes
-
# using +attr_readonly+.
-
# [:polymorphic]
-
# Specify this association is a polymorphic association by passing +true+.
-
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
-
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
-
# [:validate]
-
# If +false+, don't validate the associated objects when saving the parent object. +false+ by default.
-
# [:autosave]
-
# If true, always save the associated object or destroy it if marked for destruction, when
-
# saving the parent object.
-
# If false, never save or destroy the associated object.
-
# By default, only save the associated object if it's a new record.
-
#
-
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
-
# [:touch]
-
# If true, the associated object will be touched (the updated_at/on attributes set to current time)
-
# when this record is either saved or destroyed. If you specify a symbol, that attribute
-
# will be updated with the current time in addition to the updated_at/on attribute.
-
# [:inverse_of]
-
# Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated
-
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
-
# combination with the <tt>:polymorphic</tt> options.
-
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
-
# [:required]
-
# When set to +true+, the association will also have its presence validated.
-
# This will validate the association itself, not the id. You can use
-
# +:inverse_of+ to avoid an extra query during validation.
-
#
-
# Option examples:
-
# belongs_to :firm, foreign_key: "client_of"
-
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
-
# belongs_to :author, class_name: "Person", foreign_key: "author_id"
-
# belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
-
# class_name: "Coupon", foreign_key: "coupon_id"
-
# belongs_to :attachable, polymorphic: true
-
# belongs_to :project, -> { readonly }
-
# belongs_to :post, counter_cache: true
-
# belongs_to :company, touch: true
-
# belongs_to :company, touch: :employees_last_updated_at
-
# belongs_to :company, required: true
-
2
def belongs_to(name, scope = nil, options = {})
-
8
reflection = Builder::BelongsTo.build(self, name, scope, options)
-
8
Reflection.add_reflection self, name, reflection
-
end
-
-
# Specifies a many-to-many relationship with another class. This associates two classes via an
-
# intermediate join table. Unless the join table is explicitly specified as an option, it is
-
# guessed using the lexical order of the class names. So a join between Developer and Project
-
# will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically.
-
# Note that this precedence is calculated using the <tt><</tt> operator for String. This
-
# means that if the strings are of different lengths, and the strings are equal when compared
-
# up to the shortest length, then the longer string is considered of higher
-
# lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
-
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
-
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
-
# custom <tt>:join_table</tt> option if you need to.
-
# If your tables share a common prefix, it will only appear once at the beginning. For example,
-
# the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products".
-
#
-
# The join table should not have a primary key or a model associated with it. You must manually generate the
-
# join table with a migration such as this:
-
#
-
# class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
-
# def change
-
# create_table :developers_projects, id: false do |t|
-
# t.integer :developer_id
-
# t.integer :project_id
-
# end
-
# end
-
# end
-
#
-
# It's also a good idea to add indexes to each of those columns to speed up the joins process.
-
# However, in MySQL it is advised to add a compound index for both of the columns as MySQL only
-
# uses one index per table during the lookup.
-
#
-
# Adds the following methods for retrieval and query:
-
#
-
# +collection+ is a placeholder for the symbol passed as the +name+ argument, so
-
# <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
-
#
-
# [collection(force_reload = false)]
-
# Returns an array of all the associated objects.
-
# An empty array is returned if none are found.
-
# [collection<<(object, ...)]
-
# Adds one or more objects to the collection by creating associations in the join table
-
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
-
# Note that this operation instantly fires update SQL without waiting for the save or update call on the
-
# parent object, unless the parent object is a new record.
-
# [collection.delete(object, ...)]
-
# Removes one or more objects from the collection by removing their associations from the join table.
-
# This does not destroy the objects.
-
# [collection.destroy(object, ...)]
-
# Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option.
-
# This does not destroy the objects.
-
# [collection=objects]
-
# Replaces the collection's content by deleting and adding objects as appropriate.
-
# [collection_singular_ids]
-
# Returns an array of the associated objects' ids.
-
# [collection_singular_ids=ids]
-
# Replace the collection by the objects identified by the primary keys in +ids+.
-
# [collection.clear]
-
# Removes every object from the collection. This does not destroy the objects.
-
# [collection.empty?]
-
# Returns +true+ if there are no associated objects.
-
# [collection.size]
-
# Returns the number of associated objects.
-
# [collection.find(id)]
-
# Finds an associated object responding to the +id+ and that
-
# meets the condition that it has to be associated with this object.
-
# Uses the same rules as <tt>ActiveRecord::Base.find</tt>.
-
# [collection.exists?(...)]
-
# Checks whether an associated object with the given conditions exists.
-
# Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
-
# [collection.build(attributes = {})]
-
# Returns a new object of the collection type that has been instantiated
-
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
-
# [collection.create(attributes = {})]
-
# Returns a new object of the collection type that has been instantiated
-
# with +attributes+, linked to this object through the join table, and that has already been
-
# saved (if it passed the validation).
-
#
-
# === Example
-
#
-
# A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
-
# * <tt>Developer#projects</tt>
-
# * <tt>Developer#projects<<</tt>
-
# * <tt>Developer#projects.delete</tt>
-
# * <tt>Developer#projects.destroy</tt>
-
# * <tt>Developer#projects=</tt>
-
# * <tt>Developer#project_ids</tt>
-
# * <tt>Developer#project_ids=</tt>
-
# * <tt>Developer#projects.clear</tt>
-
# * <tt>Developer#projects.empty?</tt>
-
# * <tt>Developer#projects.size</tt>
-
# * <tt>Developer#projects.find(id)</tt>
-
# * <tt>Developer#projects.exists?(...)</tt>
-
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
-
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
-
# The declaration may include an +options+ hash to specialize the behavior of the association.
-
#
-
# === Scopes
-
#
-
# You can pass a second argument +scope+ as a callable (i.e. proc or
-
# lambda) to retrieve a specific set of records or customize the generated
-
# query when you access the associated collection.
-
#
-
# Scope examples:
-
# has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
-
# has_and_belongs_to_many :categories, ->(category) {
-
# where("default_category = ?", category.name)
-
# }
-
#
-
# === Extensions
-
#
-
# The +extension+ argument allows you to pass a block into a
-
# has_and_belongs_to_many association. This is useful for adding new
-
# finders, creators and other factory-type methods to be used as part of
-
# the association.
-
#
-
# Extension examples:
-
# has_and_belongs_to_many :contractors do
-
# def find_or_create_by_name(name)
-
# first_name, last_name = name.split(" ", 2)
-
# find_or_create_by(first_name: first_name, last_name: last_name)
-
# end
-
# end
-
#
-
# === Options
-
#
-
# [:class_name]
-
# Specify the class name of the association. Use it only if that name can't be inferred
-
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
-
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
-
# [:join_table]
-
# Specify the name of the join table if the default based on lexical order isn't what you want.
-
# <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
-
# MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work.
-
# [:foreign_key]
-
# Specify the foreign key used for the association. By default this is guessed to be the name
-
# of this class in lower-case and "_id" suffixed. So a Person class that makes
-
# a +has_and_belongs_to_many+ association to Project will use "person_id" as the
-
# default <tt>:foreign_key</tt>.
-
# [:association_foreign_key]
-
# Specify the foreign key used for the association on the receiving side of the association.
-
# By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
-
# So if a Person class makes a +has_and_belongs_to_many+ association to Project,
-
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
-
# [:readonly]
-
# If true, all the associated objects are readonly through the association.
-
# [:validate]
-
# If +false+, don't validate the associated objects when saving the parent object. +true+ by default.
-
# [:autosave]
-
# If true, always save the associated objects or destroy them if marked for destruction, when
-
# saving the parent object.
-
# If false, never save or destroy the associated objects.
-
# By default, only save associated objects that are new records.
-
#
-
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
-
#
-
# Option examples:
-
# has_and_belongs_to_many :projects
-
# has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
-
# has_and_belongs_to_many :nations, class_name: "Country"
-
# has_and_belongs_to_many :categories, join_table: "prods_cats"
-
# has_and_belongs_to_many :categories, -> { readonly }
-
2
def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
-
if scope.is_a?(Hash)
-
options = scope
-
scope = nil
-
end
-
-
habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
-
-
builder = Builder::HasAndBelongsToMany.new name, self, options
-
-
join_model = builder.through_model
-
-
# FIXME: we should move this to the internal constants. Also people
-
# should never directly access this constant so I'm not happy about
-
# setting it.
-
const_set join_model.name, join_model
-
-
middle_reflection = builder.middle_reflection join_model
-
-
Builder::HasMany.define_callbacks self, middle_reflection
-
Reflection.add_reflection self, middle_reflection.name, middle_reflection
-
middle_reflection.parent_reflection = [name.to_s, habtm_reflection]
-
-
include Module.new {
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def destroy_associations
-
association(:#{middle_reflection.name}).delete_all(:delete_all)
-
association(:#{name}).reset
-
super
-
end
-
RUBY
-
}
-
-
hm_options = {}
-
hm_options[:through] = middle_reflection.name
-
hm_options[:source] = join_model.right_reflection.name
-
-
[:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend].each do |k|
-
hm_options[k] = options[k] if options.key? k
-
end
-
-
has_many name, scope, hm_options, &extension
-
self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection]
-
end
-
end
-
end
-
end
-
1
require 'active_support/core_ext/string/conversions'
-
-
1
module ActiveRecord
-
1
module Associations
-
# Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
-
# ActiveRecord::Associations::ThroughAssociationScope
-
1
class AliasTracker # :nodoc:
-
1
attr_reader :aliases, :connection
-
-
1
def self.empty(connection)
-
38
new connection, Hash.new(0)
-
end
-
-
1
def self.create(connection, table_joins)
-
30
if table_joins.empty?
-
30
empty connection
-
else
-
aliases = Hash.new { |h,k|
-
h[k] = initial_count_for(connection, k, table_joins)
-
}
-
new connection, aliases
-
end
-
end
-
-
1
def self.initial_count_for(connection, name, table_joins)
-
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
-
quoted_name = connection.quote_table_name(name).downcase
-
-
counts = table_joins.map do |join|
-
if join.is_a?(Arel::Nodes::StringJoin)
-
# Table names + table aliases
-
join.left.downcase.scan(
-
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
-
).size
-
elsif join.respond_to? :left
-
join.left.table_name == name ? 1 : 0
-
else
-
# this branch is reached by two tests:
-
#
-
# activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
-
# with :posts
-
#
-
# activerecord/test/cases/associations/eager_test.rb:1133
-
# with :comments
-
#
-
0
-
end
-
end
-
-
counts.sum
-
end
-
-
# table_joins is an array of arel joins which might conflict with the aliases we assign here
-
1
def initialize(connection, aliases)
-
38
@aliases = aliases
-
38
@connection = connection
-
end
-
-
1
def aliased_table_for(table_name, aliased_name)
-
38
if aliases[table_name].zero?
-
# If it's zero, we can have our table_name
-
38
aliases[table_name] = 1
-
38
Arel::Table.new(table_name)
-
else
-
# Otherwise, we need to use an alias
-
aliased_name = connection.table_alias_for(aliased_name)
-
-
# Update the count
-
aliases[aliased_name] += 1
-
-
table_alias = if aliases[aliased_name] > 1
-
"#{truncate(aliased_name)}_#{aliases[aliased_name]}"
-
else
-
aliased_name
-
end
-
Arel::Table.new(table_name).alias(table_alias)
-
end
-
end
-
-
1
private
-
-
1
def truncate(name)
-
name.slice(0, connection.table_alias_length - 2)
-
end
-
end
-
end
-
end
-
1
require 'active_support/core_ext/array/wrap'
-
-
1
module ActiveRecord
-
1
module Associations
-
# = Active Record Associations
-
#
-
# This is the root class of all associations ('+ Foo' signifies an included module Foo):
-
#
-
# Association
-
# SingularAssociation
-
# HasOneAssociation
-
# HasOneThroughAssociation + ThroughAssociation
-
# BelongsToAssociation
-
# BelongsToPolymorphicAssociation
-
# CollectionAssociation
-
# HasManyAssociation
-
# HasManyThroughAssociation + ThroughAssociation
-
1
class Association #:nodoc:
-
1
attr_reader :owner, :target, :reflection
-
1
attr_accessor :inversed
-
-
1
delegate :options, :to => :reflection
-
-
1
def initialize(owner, reflection)
-
7
reflection.check_validity!
-
-
7
@owner, @reflection = owner, reflection
-
-
7
reset
-
7
reset_scope
-
end
-
-
# Returns the name of the table of the associated class:
-
#
-
# post.comments.aliased_table_name # => "comments"
-
#
-
1
def aliased_table_name
-
klass.table_name
-
end
-
-
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
-
1
def reset
-
11
@loaded = false
-
11
@target = nil
-
11
@stale_state = nil
-
11
@inversed = false
-
end
-
-
# Reloads the \target and returns +self+ on success.
-
1
def reload
-
4
reset
-
4
reset_scope
-
4
load_target
-
4
self unless target.nil?
-
end
-
-
# Has the \target been already \loaded?
-
1
def loaded?
-
15
@loaded
-
end
-
-
# Asserts the \target has been loaded setting the \loaded flag to +true+.
-
1
def loaded!
-
4
@loaded = true
-
4
@stale_state = stale_state
-
4
@inversed = false
-
end
-
-
# The target is stale if the target no longer points to the record(s) that the
-
# relevant foreign_key(s) refers to. If stale, the association accessor method
-
# on the owner will reload the target. It's up to subclasses to implement the
-
# stale_state method if relevant.
-
#
-
# Note that if the target has not been loaded, it is not considered stale.
-
1
def stale_target?
-
3
!inversed && loaded? && @stale_state != stale_state
-
end
-
-
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
-
1
def target=(target)
-
@target = target
-
loaded!
-
end
-
-
1
def scope
-
9
target_scope.merge(association_scope)
-
end
-
-
# The scope for this association.
-
#
-
# Note that the association_scope is merged into the target_scope only when the
-
# scope method is called. This is because at that point the call may be surrounded
-
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
-
# actually gets built.
-
1
def association_scope
-
9
if klass
-
9
@association_scope ||= AssociationScope.scope(self, klass.connection)
-
end
-
end
-
-
1
def reset_scope
-
11
@association_scope = nil
-
end
-
-
# Set the inverse association, if possible
-
1
def set_inverse_instance(record)
-
1
if invertible_for?(record)
-
inverse = record.association(inverse_reflection_for(record).name)
-
inverse.target = owner
-
inverse.inversed = true
-
end
-
1
record
-
end
-
-
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
-
# polymorphic_type field on the owner.
-
1
def klass
-
77
reflection.klass
-
end
-
-
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
-
# through association's scope)
-
1
def target_scope
-
11
AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
-
end
-
-
# Loads the \target if needed and returns it.
-
#
-
# This method is abstract in the sense that it relies on +find_target+,
-
# which is expected to be provided by descendants.
-
#
-
# If the \target is already \loaded it is just returned. Thus, you can call
-
# +load_target+ unconditionally to get the \target.
-
#
-
# ActiveRecord::RecordNotFound is rescued within the method, and it is
-
# not reraised. The proxy is \reset and +nil+ is the return value.
-
1
def load_target
-
4
@target = find_target if (@stale_state && stale_target?) || find_target?
-
-
4
loaded! unless loaded?
-
4
target
-
rescue ActiveRecord::RecordNotFound
-
reset
-
end
-
-
1
def interpolate(sql, record = nil)
-
if sql.respond_to?(:to_proc)
-
owner.instance_exec(record, &sql)
-
else
-
sql
-
end
-
end
-
-
# We can't dump @reflection since it contains the scope proc
-
1
def marshal_dump
-
ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
-
[@reflection.name, ivars]
-
end
-
-
1
def marshal_load(data)
-
reflection_name, ivars = data
-
ivars.each { |name, val| instance_variable_set(name, val) }
-
@reflection = @owner.class._reflect_on_association(reflection_name)
-
end
-
-
1
def initialize_attributes(record) #:nodoc:
-
skip_assign = [reflection.foreign_key, reflection.type].compact
-
attributes = create_scope.except(*(record.changed - skip_assign))
-
record.assign_attributes(attributes)
-
set_inverse_instance(record)
-
end
-
-
1
private
-
-
1
def find_target?
-
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
-
end
-
-
1
def creation_attributes
-
attributes = {}
-
-
if (reflection.has_one? || reflection.collection?) && !options[:through]
-
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
-
-
if reflection.options[:as]
-
attributes[reflection.type] = owner.class.base_class.name
-
end
-
end
-
-
attributes
-
end
-
-
# Sets the owner attributes on the given record
-
1
def set_owner_attributes(record)
-
creation_attributes.each { |key, value| record[key] = value }
-
end
-
-
# Returns true if there is a foreign key present on the owner which
-
# references the target. This is used to determine whether we can load
-
# the target if the owner is currently a new record (and therefore
-
# without a key). If the owner is a new record then foreign_key must
-
# be present in order to load target.
-
#
-
# Currently implemented by belongs_to (vanilla and polymorphic) and
-
# has_one/has_many :through associations which go through a belongs_to.
-
1
def foreign_key_present?
-
false
-
end
-
-
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
-
# the kind of the class of the associated objects. Meant to be used as
-
# a sanity check when you are about to assign an associated record.
-
1
def raise_on_type_mismatch!(record)
-
unless record.is_a?(reflection.klass)
-
fresh_class = reflection.class_name.safe_constantize
-
unless fresh_class && record.is_a?(fresh_class)
-
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
-
raise ActiveRecord::AssociationTypeMismatch, message
-
end
-
end
-
end
-
-
# Can be redefined by subclasses, notably polymorphic belongs_to
-
# The record parameter is necessary to support polymorphic inverses as we must check for
-
# the association in the specific class of the record.
-
1
def inverse_reflection_for(record)
-
1
reflection.inverse_of
-
end
-
-
# Returns true if inverse association on the given record needs to be set.
-
# This method is redefined by subclasses.
-
1
def invertible_for?(record)
-
foreign_key_for?(record) && inverse_reflection_for(record)
-
end
-
-
# Returns true if record contains the foreign_key
-
1
def foreign_key_for?(record)
-
record.has_attribute?(reflection.foreign_key)
-
end
-
-
# This should be implemented to return the values of the relevant key(s) on the owner,
-
# so that when stale_state is different from the value stored on the last find_target,
-
# the target is stale.
-
#
-
# This is only relevant to certain associations, which is why it returns nil by default.
-
1
def stale_state
-
end
-
-
1
def build_record(attributes)
-
reflection.build_association(attributes) do |record|
-
initialize_attributes(record)
-
end
-
end
-
-
# Returns true if statement cache should be skipped on the association reader.
-
1
def skip_statement_cache?
-
reflection.scope_chain.any?(&:any?) ||
-
3
scope.eager_loading? ||
-
klass.current_scope ||
-
klass.default_scopes.any? ||
-
reflection.source_reflection.active_record.default_scopes.any?
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Associations
-
1
class AssociationScope #:nodoc:
-
1
def self.scope(association, connection)
-
6
INSTANCE.scope association, connection
-
end
-
-
1
class BindSubstitution
-
1
def initialize(block)
-
3
@block = block
-
end
-
-
1
def bind_value(scope, column, value, alias_tracker)
-
8
substitute = alias_tracker.connection.substitute_at(column)
-
8
scope.bind_values += [[column, @block.call(value)]]
-
8
substitute
-
end
-
end
-
-
1
def self.create(&block)
-
9
block = block ? block : lambda { |val| val }
-
3
new BindSubstitution.new(block)
-
end
-
-
1
def initialize(bind_substitution)
-
3
@bind_substitution = bind_substitution
-
end
-
-
1
INSTANCE = create
-
-
1
def scope(association, connection)
-
8
klass = association.klass
-
8
reflection = association.reflection
-
8
scope = klass.unscoped
-
8
owner = association.owner
-
8
alias_tracker = AliasTracker.empty connection
-
-
8
scope.extending! Array(reflection.options[:extend])
-
8
add_constraints(scope, owner, klass, reflection, alias_tracker)
-
end
-
-
1
def join_type
-
Arel::Nodes::InnerJoin
-
end
-
-
1
def self.get_bind_values(owner, chain)
-
3
binds = []
-
3
last_reflection = chain.last
-
-
3
binds << last_reflection.join_id_for(owner)
-
3
if last_reflection.type
-
binds << owner.class.base_class.name
-
end
-
-
3
chain.each_cons(2).each do |reflection, next_reflection|
-
if reflection.type
-
binds << next_reflection.klass.base_class.name
-
end
-
end
-
3
binds
-
end
-
-
1
private
-
-
1
def construct_tables(chain, klass, refl, alias_tracker)
-
8
chain.map do |reflection|
-
8
alias_tracker.aliased_table_for(
-
table_name_for(reflection, klass, refl),
-
table_alias_for(reflection, refl, reflection != refl)
-
)
-
end
-
end
-
-
1
def table_alias_for(reflection, refl, join = false)
-
8
name = "#{reflection.plural_name}_#{alias_suffix(refl)}"
-
8
name << "_join" if join
-
8
name
-
end
-
-
1
def join(table, constraint)
-
table.create_join(table, table.create_on(constraint), join_type)
-
end
-
-
1
def column_for(table_name, column_name, alias_tracker)
-
8
columns = alias_tracker.connection.schema_cache.columns_hash(table_name)
-
8
columns[column_name]
-
end
-
-
1
def bind_value(scope, column, value, alias_tracker)
-
8
@bind_substitution.bind_value scope, column, value, alias_tracker
-
end
-
-
1
def bind(scope, table_name, column_name, value, tracker)
-
8
column = column_for table_name, column_name, tracker
-
8
bind_value scope, column, value, tracker
-
end
-
-
1
def last_chain_scope(scope, table, reflection, owner, tracker, assoc_klass)
-
8
join_keys = reflection.join_keys(assoc_klass)
-
8
key = join_keys.key
-
8
foreign_key = join_keys.foreign_key
-
-
8
bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
-
8
scope = scope.where(table[key].eq(bind_val))
-
-
8
if reflection.type
-
value = owner.class.base_class.name
-
bind_val = bind scope, table.table_name, reflection.type, value, tracker
-
scope = scope.where(table[reflection.type].eq(bind_val))
-
else
-
8
scope
-
end
-
end
-
-
1
def next_chain_scope(scope, table, reflection, tracker, assoc_klass, foreign_table, next_reflection)
-
join_keys = reflection.join_keys(assoc_klass)
-
key = join_keys.key
-
foreign_key = join_keys.foreign_key
-
-
constraint = table[key].eq(foreign_table[foreign_key])
-
-
if reflection.type
-
value = next_reflection.klass.base_class.name
-
bind_val = bind scope, table.table_name, reflection.type, value, tracker
-
scope = scope.where(table[reflection.type].eq(bind_val))
-
end
-
-
scope = scope.joins(join(foreign_table, constraint))
-
end
-
-
1
def add_constraints(scope, owner, assoc_klass, refl, tracker)
-
8
chain = refl.chain
-
8
scope_chain = refl.scope_chain
-
-
8
tables = construct_tables(chain, assoc_klass, refl, tracker)
-
-
8
owner_reflection = chain.last
-
8
table = tables.last
-
8
scope = last_chain_scope(scope, table, owner_reflection, owner, tracker, assoc_klass)
-
-
8
chain.each_with_index do |reflection, i|
-
8
table, foreign_table = tables.shift, tables.first
-
-
8
unless reflection == chain.last
-
next_reflection = chain[i + 1]
-
scope = next_chain_scope(scope, table, reflection, tracker, assoc_klass, foreign_table, next_reflection)
-
end
-
-
8
is_first_chain = i == 0
-
8
klass = is_first_chain ? assoc_klass : reflection.klass
-
-
# Exclude the scope of the association itself, because that
-
# was already merged in the #scope method.
-
8
scope_chain[i].each do |scope_chain_item|
-
item = eval_scope(klass, scope_chain_item, owner)
-
-
if scope_chain_item == refl.scope
-
scope.merge! item.except(:where, :includes, :bind)
-
end
-
-
if is_first_chain
-
scope.includes! item.includes_values
-
end
-
-
scope.unscope!(*item.unscope_values)
-
scope.where_values += item.where_values
-
scope.bind_values += item.bind_values
-
scope.order_values |= item.order_values
-
end
-
end
-
-
8
scope
-
end
-
-
1
def alias_suffix(refl)
-
8
refl.name
-
end
-
-
1
def table_name_for(reflection, klass, refl)
-
8
if reflection == refl
-
# If this is a polymorphic belongs_to, we want to get the klass from the
-
# association because it depends on the polymorphic_type attribute of
-
# the owner
-
8
klass.table_name
-
else
-
reflection.table_name
-
end
-
end
-
-
1
def eval_scope(klass, scope, owner)
-
klass.unscoped.instance_exec(owner, &scope)
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
# = Active Record Belongs To Association
-
1
module Associations
-
1
class BelongsToAssociation < SingularAssociation #:nodoc:
-
-
1
def handle_dependency
-
target.send(options[:dependent]) if load_target
-
end
-
-
1
def replace(record)
-
if record
-
raise_on_type_mismatch!(record)
-
update_counters(record)
-
replace_keys(record)
-
set_inverse_instance(record)
-
@updated = true
-
else
-
decrement_counters
-
remove_keys
-
end
-
-
self.target = record
-
end
-
-
1
def reset
-
8
super
-
8
@updated = false
-
end
-
-
1
def updated?
-
@updated
-
end
-
-
1
def decrement_counters # :nodoc:
-
with_cache_name { |name| decrement_counter name }
-
end
-
-
1
def increment_counters # :nodoc:
-
with_cache_name { |name| increment_counter name }
-
end
-
-
1
private
-
-
1
def find_target?
-
4
!loaded? && foreign_key_present? && klass
-
end
-
-
1
def with_cache_name
-
counter_cache_name = reflection.counter_cache_column
-
return unless counter_cache_name && owner.persisted?
-
yield counter_cache_name
-
end
-
-
1
def update_counters(record)
-
with_cache_name do |name|
-
return unless different_target? record
-
record.class.increment_counter(name, record.id)
-
decrement_counter name
-
end
-
end
-
-
1
def decrement_counter(counter_cache_name)
-
if foreign_key_present?
-
klass.decrement_counter(counter_cache_name, target_id)
-
end
-
end
-
-
1
def increment_counter(counter_cache_name)
-
if foreign_key_present?
-
klass.increment_counter(counter_cache_name, target_id)
-
if target && !stale_target? && counter_cache_available_in_memory?(counter_cache_name)
-
target.increment(counter_cache_name)
-
end
-
end
-
end
-
-
# Checks whether record is different to the current target, without loading it
-
1
def different_target?(record)
-
record.id != owner._read_attribute(reflection.foreign_key)
-
end
-
-
1
def replace_keys(record)
-
owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class))
-
end
-
-
1
def remove_keys
-
owner[reflection.foreign_key] = nil
-
end
-
-
1
def foreign_key_present?
-
4
owner._read_attribute(reflection.foreign_key)
-
end
-
-
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
-
# has_one associations.
-
1
def invertible_for?(record)
-
1
inverse = inverse_reflection_for(record)
-
1
inverse && inverse.has_one?
-
end
-
-
1
def target_id
-
if options[:primary_key]
-
owner.send(reflection.name).try(:id)
-
else
-
owner._read_attribute(reflection.foreign_key)
-
end
-
end
-
-
1
def stale_state
-
4
result = owner._read_attribute(reflection.foreign_key)
-
4
result && result.to_s
-
end
-
-
1
def counter_cache_available_in_memory?(counter_cache_name)
-
target.respond_to?(counter_cache_name)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/attribute_accessors'
-
-
# This is the parent Association class which defines the variables
-
# used by all associations.
-
#
-
# The hierarchy is defined as follows:
-
# Association
-
# - SingularAssociation
-
# - BelongsToAssociation
-
# - HasOneAssociation
-
# - CollectionAssociation
-
# - HasManyAssociation
-
-
2
module ActiveRecord::Associations::Builder
-
2
class Association #:nodoc:
-
2
class << self
-
2
attr_accessor :extensions
-
# TODO: This class accessor is needed to make activerecord-deprecated_finders work.
-
# We can move it to a constant in 5.0.
-
2
attr_accessor :valid_options
-
end
-
2
self.extensions = []
-
-
2
self.valid_options = [:class_name, :anonymous_class, :foreign_key, :validate]
-
-
2
attr_reader :name, :scope, :options
-
-
2
def self.build(model, name, scope, options, &block)
-
24
if model.dangerous_attribute_method?(name)
-
raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
-
"this will conflict with a method #{name} already defined by Active Record. " \
-
"Please choose a different association name."
-
end
-
-
24
builder = create_builder model, name, scope, options, &block
-
24
reflection = builder.build(model)
-
24
define_accessors model, reflection
-
24
define_callbacks model, reflection
-
24
define_validations model, reflection
-
24
builder.define_extensions model
-
24
reflection
-
end
-
-
2
def self.create_builder(model, name, scope, options, &block)
-
24
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
-
-
24
new(model, name, scope, options, &block)
-
end
-
-
2
def initialize(model, name, scope, options)
-
# TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
-
24
if scope.is_a?(Hash)
-
8
options = scope
-
8
scope = nil
-
end
-
-
# TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
-
24
@name = name
-
24
@scope = scope
-
24
@options = options
-
-
24
validate_options
-
-
24
if scope && scope.arity == 0
-
@scope = proc { instance_exec(&scope) }
-
end
-
end
-
-
2
def build(model)
-
24
ActiveRecord::Reflection.create(macro, name, scope, options, model)
-
end
-
-
2
def macro
-
raise NotImplementedError
-
end
-
-
2
def valid_options
-
24
Association.valid_options + Association.extensions.flat_map(&:valid_options)
-
end
-
-
2
def validate_options
-
24
options.assert_valid_keys(valid_options)
-
end
-
-
2
def define_extensions(model)
-
end
-
-
2
def self.define_callbacks(model, reflection)
-
24
if dependent = reflection.options[:dependent]
-
check_dependent_options(dependent)
-
add_destroy_callbacks(model, reflection)
-
end
-
-
24
Association.extensions.each do |extension|
-
24
extension.build model, reflection
-
end
-
end
-
-
# Defines the setter and getter methods for the association
-
# class Post < ActiveRecord::Base
-
# has_many :comments
-
# end
-
#
-
# Post.first.comments and Post.first.comments= methods are defined by this method...
-
2
def self.define_accessors(model, reflection)
-
24
mixin = model.generated_association_methods
-
24
name = reflection.name
-
24
define_readers(mixin, name)
-
24
define_writers(mixin, name)
-
end
-
-
2
def self.define_readers(mixin, name)
-
24
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name}(*args)
-
association(:#{name}).reader(*args)
-
end
-
CODE
-
end
-
-
2
def self.define_writers(mixin, name)
-
24
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name}=(value)
-
association(:#{name}).writer(value)
-
end
-
CODE
-
end
-
-
2
def self.define_validations(model, reflection)
-
# noop
-
end
-
-
2
def self.valid_dependent_options
-
raise NotImplementedError
-
end
-
-
2
private
-
-
2
def self.check_dependent_options(dependent)
-
unless valid_dependent_options.include? dependent
-
raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
-
end
-
end
-
-
2
def self.add_destroy_callbacks(model, reflection)
-
name = reflection.name
-
model.before_destroy lambda { |o| o.association(name).handle_dependency }
-
end
-
end
-
end
-
2
module ActiveRecord::Associations::Builder
-
2
class BelongsTo < SingularAssociation #:nodoc:
-
2
def macro
-
8
:belongs_to
-
end
-
-
2
def valid_options
-
8
super + [:foreign_type, :polymorphic, :touch, :counter_cache]
-
end
-
-
2
def self.valid_dependent_options
-
[:destroy, :delete]
-
end
-
-
2
def self.define_callbacks(model, reflection)
-
8
super
-
8
add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
-
8
add_touch_callbacks(model, reflection) if reflection.options[:touch]
-
end
-
-
2
def self.define_accessors(mixin, reflection)
-
8
super
-
8
add_counter_cache_methods mixin
-
end
-
-
2
private
-
-
2
def self.add_counter_cache_methods(mixin)
-
8
return if mixin.method_defined? :belongs_to_counter_cache_after_update
-
-
4
mixin.class_eval do
-
4
def belongs_to_counter_cache_after_update(reflection)
-
foreign_key = reflection.foreign_key
-
cache_column = reflection.counter_cache_column
-
-
if (@_after_create_counter_called ||= false)
-
@_after_create_counter_called = false
-
elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
-
model = reflection.klass
-
foreign_key_was = attribute_was foreign_key
-
foreign_key = attribute foreign_key
-
-
if foreign_key && model.respond_to?(:increment_counter)
-
model.increment_counter(cache_column, foreign_key)
-
end
-
if foreign_key_was && model.respond_to?(:decrement_counter)
-
model.decrement_counter(cache_column, foreign_key_was)
-
end
-
end
-
end
-
end
-
end
-
-
2
def self.add_counter_cache_callbacks(model, reflection)
-
cache_column = reflection.counter_cache_column
-
-
model.after_update lambda { |record|
-
record.belongs_to_counter_cache_after_update(reflection)
-
}
-
-
klass = reflection.class_name.safe_constantize
-
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
-
end
-
-
2
def self.touch_record(o, foreign_key, name, touch) # :nodoc:
-
old_foreign_id = o.changed_attributes[foreign_key]
-
-
if old_foreign_id
-
association = o.association(name)
-
reflection = association.reflection
-
if reflection.polymorphic?
-
klass = o.public_send("#{reflection.foreign_type}_was").constantize
-
else
-
klass = association.klass
-
end
-
old_record = klass.find_by(klass.primary_key => old_foreign_id)
-
-
if old_record
-
if touch != true
-
old_record.touch touch
-
else
-
old_record.touch
-
end
-
end
-
end
-
-
record = o.send name
-
if record && record.persisted?
-
if touch != true
-
record.touch touch
-
else
-
record.touch
-
end
-
end
-
end
-
-
2
def self.add_touch_callbacks(model, reflection)
-
foreign_key = reflection.foreign_key
-
n = reflection.name
-
touch = reflection.options[:touch]
-
-
callback = lambda { |record|
-
BelongsTo.touch_record(record, foreign_key, n, touch)
-
}
-
-
model.after_save callback, if: :changed?
-
model.after_touch callback
-
model.after_destroy callback
-
end
-
-
2
def self.add_destroy_callbacks(model, reflection)
-
name = reflection.name
-
model.after_destroy lambda { |o| o.association(name).handle_dependency }
-
end
-
end
-
end
-
# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
-
-
2
require 'active_record/associations'
-
-
2
module ActiveRecord::Associations::Builder
-
2
class CollectionAssociation < Association #:nodoc:
-
-
2
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
-
-
2
def valid_options
-
super + [:table_name, :before_add,
-
16
:after_add, :before_remove, :after_remove, :extend]
-
end
-
-
2
attr_reader :block_extension
-
-
2
def initialize(model, name, scope, options)
-
16
super
-
16
@mod = nil
-
16
if block_given?
-
@mod = Module.new(&Proc.new)
-
@scope = wrap_scope @scope, @mod
-
end
-
end
-
-
2
def self.define_callbacks(model, reflection)
-
16
super
-
16
name = reflection.name
-
16
options = reflection.options
-
16
CALLBACKS.each { |callback_name|
-
64
define_callback(model, callback_name, name, options)
-
}
-
end
-
-
2
def define_extensions(model)
-
16
if @mod
-
extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
-
model.parent.const_set(extension_module_name, @mod)
-
end
-
end
-
-
2
def self.define_callback(model, callback_name, name, options)
-
64
full_callback_name = "#{callback_name}_for_#{name}"
-
-
# TODO : why do i need method_defined? I think its because of the inheritance chain
-
64
model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
-
64
callbacks = Array(options[callback_name.to_sym]).map do |callback|
-
case callback
-
when Symbol
-
->(method, owner, record) { owner.send(callback, record) }
-
when Proc
-
->(method, owner, record) { callback.call(owner, record) }
-
else
-
->(method, owner, record) { callback.send(method, owner, record) }
-
end
-
end
-
64
model.send "#{full_callback_name}=", callbacks
-
end
-
-
# Defines the setter and getter methods for the collection_singular_ids.
-
2
def self.define_readers(mixin, name)
-
16
super
-
-
16
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name.to_s.singularize}_ids
-
association(:#{name}).ids_reader
-
end
-
CODE
-
end
-
-
2
def self.define_writers(mixin, name)
-
16
super
-
-
16
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name.to_s.singularize}_ids=(ids)
-
association(:#{name}).ids_writer(ids)
-
end
-
CODE
-
end
-
-
2
private
-
-
2
def wrap_scope(scope, mod)
-
if scope
-
if scope.arity > 0
-
proc { |owner| instance_exec(owner, &scope).extending(mod) }
-
else
-
proc { instance_exec(&scope).extending(mod) }
-
end
-
else
-
proc { extending(mod) }
-
end
-
end
-
end
-
end
-
2
module ActiveRecord::Associations::Builder
-
2
class HasMany < CollectionAssociation #:nodoc:
-
2
def macro
-
16
:has_many
-
end
-
-
2
def valid_options
-
16
super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
-
end
-
-
2
def self.valid_dependent_options
-
[:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
-
end
-
end
-
end
-
# This class is inherited by the has_one and belongs_to association classes
-
-
2
module ActiveRecord::Associations::Builder
-
2
class SingularAssociation < Association #:nodoc:
-
2
def valid_options
-
8
super + [:dependent, :primary_key, :inverse_of, :required]
-
end
-
-
2
def self.define_accessors(model, reflection)
-
8
super
-
8
define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
-
end
-
-
# Defines the (build|create)_association methods for belongs_to or has_one association
-
2
def self.define_constructors(mixin, name)
-
8
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def build_#{name}(*args, &block)
-
association(:#{name}).build(*args, &block)
-
end
-
-
def create_#{name}(*args, &block)
-
association(:#{name}).create(*args, &block)
-
end
-
-
def create_#{name}!(*args, &block)
-
association(:#{name}).create!(*args, &block)
-
end
-
CODE
-
end
-
-
2
def self.define_validations(model, reflection)
-
8
super
-
8
if reflection.options[:required]
-
model.validates_presence_of reflection.name
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Associations
-
# = Active Record Association Collection
-
#
-
# CollectionAssociation is an abstract class that provides common stuff to
-
# ease the implementation of association proxies that represent
-
# collections. See the class hierarchy in Association.
-
#
-
# CollectionAssociation:
-
# HasManyAssociation => has_many
-
# HasManyThroughAssociation + ThroughAssociation => has_many :through
-
#
-
# CollectionAssociation class provides common methods to the collections
-
# defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
-
# +:through association+ option.
-
#
-
# You need to be careful with assumptions regarding the target: The proxy
-
# does not fetch records from the database until it needs them, but new
-
# ones created with +build+ are added to the target. So, the target may be
-
# non-empty and still lack children waiting to be read from the database.
-
# If you look directly to the database you cannot assume that's the entire
-
# collection because new records may have been added to the target, etc.
-
#
-
# If you need to work on all current children, new and existing records,
-
# +load_target+ and the +loaded+ flag are your friends.
-
1
class CollectionAssociation < Association #:nodoc:
-
-
# Implements the reader method, e.g. foo.items for Foo.has_many :items
-
1
def reader(force_reload = false)
-
3
if force_reload
-
klass.uncached { reload }
-
elsif stale_target?
-
reload
-
end
-
-
3
if owner.new_record?
-
# Cache the proxy separately before the owner has an id
-
# or else a post-save proxy will still lack the id
-
@new_record_proxy ||= CollectionProxy.create(klass, self)
-
else
-
3
@proxy ||= CollectionProxy.create(klass, self)
-
end
-
end
-
-
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
-
1
def writer(records)
-
replace(records)
-
end
-
-
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
-
1
def ids_reader
-
if loaded?
-
load_target.map do |record|
-
record.send(reflection.association_primary_key)
-
end
-
else
-
column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
-
scope.pluck(column)
-
end
-
end
-
-
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
-
1
def ids_writer(ids)
-
pk_type = reflection.primary_key_type
-
ids = Array(ids).reject { |id| id.blank? }
-
ids.map! { |i| pk_type.type_cast_from_user(i) }
-
replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
-
end
-
-
1
def reset
-
3
super
-
3
@target = []
-
end
-
-
1
def select(*fields)
-
if block_given?
-
load_target.select.each { |e| yield e }
-
else
-
scope.select(*fields)
-
end
-
end
-
-
1
def find(*args)
-
if block_given?
-
load_target.find(*args) { |*block_args| yield(*block_args) }
-
else
-
if options[:inverse_of] && loaded?
-
args_flatten = args.flatten
-
raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
-
result = find_by_scan(*args)
-
-
result_size = Array(result).size
-
if !result || result_size != args_flatten.size
-
scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
-
else
-
result
-
end
-
else
-
scope.find(*args)
-
end
-
end
-
end
-
-
1
def first(*args)
-
first_nth_or_last(:first, *args)
-
end
-
-
1
def second(*args)
-
first_nth_or_last(:second, *args)
-
end
-
-
1
def third(*args)
-
first_nth_or_last(:third, *args)
-
end
-
-
1
def fourth(*args)
-
first_nth_or_last(:fourth, *args)
-
end
-
-
1
def fifth(*args)
-
first_nth_or_last(:fifth, *args)
-
end
-
-
1
def forty_two(*args)
-
first_nth_or_last(:forty_two, *args)
-
end
-
-
1
def last(*args)
-
first_nth_or_last(:last, *args)
-
end
-
-
1
def take(n = nil)
-
if loaded?
-
n ? target.take(n) : target.first
-
else
-
scope.take(n).tap do |record|
-
set_inverse_instance record if record.is_a? ActiveRecord::Base
-
end
-
end
-
end
-
-
1
def build(attributes = {}, &block)
-
if attributes.is_a?(Array)
-
attributes.collect { |attr| build(attr, &block) }
-
else
-
add_to_target(build_record(attributes)) do |record|
-
yield(record) if block_given?
-
end
-
end
-
end
-
-
1
def create(attributes = {}, &block)
-
_create_record(attributes, &block)
-
end
-
-
1
def create!(attributes = {}, &block)
-
_create_record(attributes, true, &block)
-
end
-
-
# Add +records+ to this association. Returns +self+ so method calls may
-
# be chained. Since << flattens its argument list and inserts each record,
-
# +push+ and +concat+ behave identically.
-
1
def concat(*records)
-
if owner.new_record?
-
load_target
-
concat_records(records)
-
else
-
transaction { concat_records(records) }
-
end
-
end
-
-
# Starts a transaction in the association class's database connection.
-
#
-
# class Author < ActiveRecord::Base
-
# has_many :books
-
# end
-
#
-
# Author.first.books.transaction do
-
# # same effect as calling Book.transaction
-
# end
-
1
def transaction(*args)
-
reflection.klass.transaction(*args) do
-
yield
-
end
-
end
-
-
# Removes all records from the association without calling callbacks
-
# on the associated records. It honors the +:dependent+ option. However
-
# if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
-
# deletion strategy for the association is applied.
-
#
-
# You can force a particular deletion strategy by passing a parameter.
-
#
-
# Example:
-
#
-
# @author.books.delete_all(:nullify)
-
# @author.books.delete_all(:delete_all)
-
#
-
# See delete for more info.
-
1
def delete_all(dependent = nil)
-
if dependent && ![:nullify, :delete_all].include?(dependent)
-
raise ArgumentError, "Valid values are :nullify or :delete_all"
-
end
-
-
dependent = if dependent
-
dependent
-
elsif options[:dependent] == :destroy
-
:delete_all
-
else
-
options[:dependent]
-
end
-
-
delete_or_nullify_all_records(dependent).tap do
-
reset
-
loaded!
-
end
-
end
-
-
# Destroy all the records from this association.
-
#
-
# See destroy for more info.
-
1
def destroy_all
-
destroy(load_target).tap do
-
reset
-
loaded!
-
end
-
end
-
-
# Count all records using SQL. Construct options and pass them with
-
# scope to the target class's +count+.
-
1
def count(column_name = nil, count_options = {})
-
# TODO: Remove count_options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
column_name, count_options = nil, column_name if column_name.is_a?(Hash)
-
-
relation = scope
-
if association_scope.distinct_value
-
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
-
column_name ||= reflection.klass.primary_key
-
relation = relation.distinct
-
end
-
-
value = relation.count(column_name)
-
-
limit = options[:limit]
-
offset = options[:offset]
-
-
if limit || offset
-
[ [value - offset.to_i, 0].max, limit.to_i ].min
-
else
-
value
-
end
-
end
-
-
# Removes +records+ from this association calling +before_remove+ and
-
# +after_remove+ callbacks.
-
#
-
# This method is abstract in the sense that +delete_records+ has to be
-
# provided by descendants. Note this method does not imply the records
-
# are actually removed from the database, that depends precisely on
-
# +delete_records+. They are in any case removed from the collection.
-
1
def delete(*records)
-
return if records.empty?
-
_options = records.extract_options!
-
dependent = _options[:dependent] || options[:dependent]
-
-
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
-
delete_or_destroy(records, dependent)
-
end
-
-
# Deletes the +records+ and removes them from this association calling
-
# +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
-
#
-
# Note that this method removes records from the database ignoring the
-
# +:dependent+ option.
-
1
def destroy(*records)
-
return if records.empty?
-
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
-
delete_or_destroy(records, :destroy)
-
end
-
-
# Returns the size of the collection by executing a SELECT COUNT(*)
-
# query if the collection hasn't been loaded, and calling
-
# <tt>collection.size</tt> if it has.
-
#
-
# If the collection has been already loaded +size+ and +length+ are
-
# equivalent. If not and you are going to need the records anyway
-
# +length+ will take one less query. Otherwise +size+ is more efficient.
-
#
-
# This method is abstract in the sense that it relies on
-
# +count_records+, which is a method descendants have to provide.
-
1
def size
-
if !find_target? || loaded?
-
if association_scope.distinct_value
-
target.uniq.size
-
else
-
target.size
-
end
-
elsif !loaded? && !association_scope.group_values.empty?
-
load_target.size
-
elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
-
unsaved_records = target.select { |r| r.new_record? }
-
unsaved_records.size + count_records
-
else
-
count_records
-
end
-
end
-
-
# Returns the size of the collection calling +size+ on the target.
-
#
-
# If the collection has been already loaded +length+ and +size+ are
-
# equivalent. If not and you are going to need the records anyway this
-
# method will take one less query. Otherwise +size+ is more efficient.
-
1
def length
-
load_target.size
-
end
-
-
# Returns true if the collection is empty.
-
#
-
# If the collection has been loaded
-
# it is equivalent to <tt>collection.size.zero?</tt>. If the
-
# collection has not been loaded, it is equivalent to
-
# <tt>collection.exists?</tt>. If the collection has not already been
-
# loaded and you are going to fetch the records anyway it is better to
-
# check <tt>collection.length.zero?</tt>.
-
1
def empty?
-
if loaded?
-
size.zero?
-
else
-
@target.blank? && !scope.exists?
-
end
-
end
-
-
# Returns true if the collections is not empty.
-
# Equivalent to +!collection.empty?+.
-
1
def any?
-
if block_given?
-
load_target.any? { |*block_args| yield(*block_args) }
-
else
-
!empty?
-
end
-
end
-
-
# Returns true if the collection has more than 1 record.
-
# Equivalent to +collection.size > 1+.
-
1
def many?
-
if block_given?
-
load_target.many? { |*block_args| yield(*block_args) }
-
else
-
size > 1
-
end
-
end
-
-
1
def distinct
-
seen = {}
-
load_target.find_all do |record|
-
seen[record.id] = true unless seen.key?(record.id)
-
end
-
end
-
1
alias uniq distinct
-
-
# Replace this collection with +other_array+. This will perform a diff
-
# and delete/add only records that have changed.
-
1
def replace(other_array)
-
other_array.each { |val| raise_on_type_mismatch!(val) }
-
original_target = load_target.dup
-
-
if owner.new_record?
-
replace_records(other_array, original_target)
-
else
-
replace_common_records_in_memory(other_array, original_target)
-
if other_array != original_target
-
transaction { replace_records(other_array, original_target) }
-
end
-
end
-
end
-
-
1
def include?(record)
-
if record.is_a?(reflection.klass)
-
if record.new_record?
-
include_in_memory?(record)
-
else
-
loaded? ? target.include?(record) : scope.exists?(record.id)
-
end
-
else
-
false
-
end
-
end
-
-
1
def load_target
-
if find_target?
-
@target = merge_target_lists(find_target, target)
-
end
-
-
loaded!
-
target
-
end
-
-
1
def add_to_target(record, skip_callbacks = false, &block)
-
if association_scope.distinct_value
-
index = @target.index(record)
-
end
-
replace_on_target(record, index, skip_callbacks, &block)
-
end
-
-
1
def replace_on_target(record, index, skip_callbacks)
-
callback(:before_add, record) unless skip_callbacks
-
yield(record) if block_given?
-
-
if index
-
@target[index] = record
-
else
-
@target << record
-
end
-
-
callback(:after_add, record) unless skip_callbacks
-
set_inverse_instance(record)
-
-
record
-
end
-
-
1
def scope(opts = {})
-
6
scope = super()
-
6
scope.none! if opts.fetch(:nullify, true) && null_scope?
-
6
scope
-
end
-
-
1
def null_scope?
-
3
owner.new_record? && !foreign_key_present?
-
end
-
-
1
private
-
1
def get_records
-
return scope.to_a if skip_statement_cache?
-
-
conn = klass.connection
-
sc = reflection.association_scope_cache(conn, owner) do
-
StatementCache.create(conn) { |params|
-
as = AssociationScope.create { params.bind }
-
target_scope.merge as.scope(self, conn)
-
}
-
end
-
-
binds = AssociationScope.get_bind_values(owner, reflection.chain)
-
sc.execute binds, klass, klass.connection
-
end
-
-
1
def find_target
-
records = get_records
-
records.each { |record| set_inverse_instance(record) }
-
records
-
end
-
-
# We have some records loaded from the database (persisted) and some that are
-
# in-memory (memory). The same record may be represented in the persisted array
-
# and in the memory array.
-
#
-
# So the task of this method is to merge them according to the following rules:
-
#
-
# * The final array must not have duplicates
-
# * The order of the persisted array is to be preserved
-
# * Any changes made to attributes on objects in the memory array are to be preserved
-
# * Otherwise, attributes should have the value found in the database
-
1
def merge_target_lists(persisted, memory)
-
return persisted if memory.empty?
-
return memory if persisted.empty?
-
-
persisted.map! do |record|
-
if mem_record = memory.delete(record)
-
-
((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name|
-
mem_record[name] = record[name]
-
end
-
-
mem_record
-
else
-
record
-
end
-
end
-
-
persisted + memory
-
end
-
-
1
def _create_record(attributes, raise = false, &block)
-
unless owner.persisted?
-
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
-
end
-
-
if attributes.is_a?(Array)
-
attributes.collect { |attr| _create_record(attr, raise, &block) }
-
else
-
transaction do
-
add_to_target(build_record(attributes)) do |record|
-
yield(record) if block_given?
-
insert_record(record, true, raise)
-
end
-
end
-
end
-
end
-
-
# Do the relevant stuff to insert the given record into the association collection.
-
1
def insert_record(record, validate = true, raise = false)
-
raise NotImplementedError
-
end
-
-
1
def create_scope
-
scope.scope_for_create.stringify_keys
-
end
-
-
1
def delete_or_destroy(records, method)
-
records = records.flatten
-
records.each { |record| raise_on_type_mismatch!(record) }
-
existing_records = records.reject { |r| r.new_record? }
-
-
if existing_records.empty?
-
remove_records(existing_records, records, method)
-
else
-
transaction { remove_records(existing_records, records, method) }
-
end
-
end
-
-
1
def remove_records(existing_records, records, method)
-
records.each { |record| callback(:before_remove, record) }
-
-
delete_records(existing_records, method) if existing_records.any?
-
records.each { |record| target.delete(record) }
-
-
records.each { |record| callback(:after_remove, record) }
-
end
-
-
# Delete the given records from the association, using one of the methods :destroy,
-
# :delete_all or :nullify (or nil, in which case a default is used).
-
1
def delete_records(records, method)
-
raise NotImplementedError
-
end
-
-
1
def replace_records(new_target, original_target)
-
delete(target - new_target)
-
-
unless concat(new_target - target)
-
@target = original_target
-
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
-
"new records could not be saved."
-
end
-
-
target
-
end
-
-
1
def replace_common_records_in_memory(new_target, original_target)
-
common_records = new_target & original_target
-
common_records.each do |record|
-
skip_callbacks = true
-
replace_on_target(record, @target.index(record), skip_callbacks)
-
end
-
end
-
-
1
def concat_records(records, should_raise = false)
-
result = true
-
-
records.flatten.each do |record|
-
raise_on_type_mismatch!(record)
-
add_to_target(record) do |rec|
-
result &&= insert_record(rec, true, should_raise) unless owner.new_record?
-
end
-
end
-
-
result && records
-
end
-
-
1
def callback(method, record)
-
callbacks_for(method).each do |callback|
-
callback.call(method, owner, record)
-
end
-
end
-
-
1
def callbacks_for(callback_name)
-
full_callback_name = "#{callback_name}_for_#{reflection.name}"
-
owner.class.send(full_callback_name)
-
end
-
-
# Should we deal with assoc.first or assoc.last by issuing an independent query to
-
# the database, or by getting the target, and then taking the first/last item from that?
-
#
-
# If the args is just a non-empty options hash, go to the database.
-
#
-
# Otherwise, go to the database only if none of the following are true:
-
# * target already loaded
-
# * owner is new record
-
# * target contains new or changed record(s)
-
1
def fetch_first_nth_or_last_using_find?(args)
-
if args.first.is_a?(Hash)
-
true
-
else
-
!(loaded? ||
-
owner.new_record? ||
-
target.any? { |record| record.new_record? || record.changed? })
-
end
-
end
-
-
1
def include_in_memory?(record)
-
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
-
assoc = owner.association(reflection.through_reflection.name)
-
assoc.reader.any? { |source|
-
target_association = source.send(reflection.source_reflection.name)
-
-
if target_association.respond_to?(:include?)
-
target_association.include?(record)
-
else
-
target_association == record
-
end
-
} || target.include?(record)
-
else
-
target.include?(record)
-
end
-
end
-
-
# If the :inverse_of option has been
-
# specified, then #find scans the entire collection.
-
1
def find_by_scan(*args)
-
expects_array = args.first.kind_of?(Array)
-
ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq
-
-
if ids.size == 1
-
id = ids.first
-
record = load_target.detect { |r| id == r.id.to_s }
-
expects_array ? [ record ] : record
-
else
-
load_target.select { |r| ids.include?(r.id.to_s) }
-
end
-
end
-
-
# Fetches the first/last using SQL if possible, otherwise from the target array.
-
1
def first_nth_or_last(type, *args)
-
args.shift if args.first.is_a?(Hash) && args.first.empty?
-
-
collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target
-
collection.send(type, *args).tap do |record|
-
set_inverse_instance record if record.is_a? ActiveRecord::Base
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Associations
-
# Association proxies in Active Record are middlemen between the object that
-
# holds the association, known as the <tt>@owner</tt>, and the actual associated
-
# object, known as the <tt>@target</tt>. The kind of association any proxy is
-
# about is available in <tt>@reflection</tt>. That's an instance of the class
-
# ActiveRecord::Reflection::AssociationReflection.
-
#
-
# For example, given
-
#
-
# class Blog < ActiveRecord::Base
-
# has_many :posts
-
# end
-
#
-
# blog = Blog.first
-
#
-
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
-
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
-
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
-
#
-
# This class delegates unknown methods to <tt>@target</tt> via
-
# <tt>method_missing</tt>.
-
#
-
# The <tt>@target</tt> object is not \loaded until needed. For example,
-
#
-
# blog.posts.count
-
#
-
# is computed directly through SQL and does not trigger by itself the
-
# instantiation of the actual post records.
-
2
class CollectionProxy < Relation
-
2
delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope)
-
2
delegate :find_nth, to: :scope
-
-
2
def initialize(klass, association) #:nodoc:
-
3
@association = association
-
3
super klass, klass.arel_table
-
3
merge! association.scope(nullify: false)
-
end
-
-
2
def target
-
@association.target
-
end
-
-
2
def load_target
-
@association.load_target
-
end
-
-
# Returns +true+ if the association has been loaded, otherwise +false+.
-
#
-
# person.pets.loaded? # => false
-
# person.pets
-
# person.pets.loaded? # => true
-
2
def loaded?
-
@association.loaded?
-
end
-
-
# Works in two ways.
-
#
-
# *First:* Specify a subset of fields to be selected from the result set.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.select(:name)
-
# # => [
-
# # #<Pet id: nil, name: "Fancy-Fancy">,
-
# # #<Pet id: nil, name: "Spook">,
-
# # #<Pet id: nil, name: "Choo-Choo">
-
# # ]
-
#
-
# person.pets.select(:id, :name )
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy">,
-
# # #<Pet id: 2, name: "Spook">,
-
# # #<Pet id: 3, name: "Choo-Choo">
-
# # ]
-
#
-
# Be careful because this also means you're initializing a model
-
# object with only the fields that you've selected. If you attempt
-
# to access a field except +id+ that is not in the initialized record you'll
-
# receive:
-
#
-
# person.pets.select(:name).first.person_id
-
# # => ActiveModel::MissingAttributeError: missing attribute: person_id
-
#
-
# *Second:* You can pass a block so it can be used just like Array#select.
-
# This builds an array of objects from the database for the scope,
-
# converting them into an array and iterating through them using
-
# Array#select.
-
#
-
# person.pets.select { |pet| pet.name =~ /oo/ }
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.select(:name) { |pet| pet.name =~ /oo/ }
-
# # => [
-
# # #<Pet id: 2, name: "Spook">,
-
# # #<Pet id: 3, name: "Choo-Choo">
-
# # ]
-
2
def select(*fields, &block)
-
@association.select(*fields, &block)
-
end
-
-
# Finds an object in the collection responding to the +id+. Uses the same
-
# rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt>
-
# error if the object cannot be found.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
-
# person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
-
#
-
# person.pets.find(2) { |pet| pet.name.downcase! }
-
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
-
#
-
# person.pets.find(2, 3)
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
2
def find(*args, &block)
-
@association.find(*args, &block)
-
end
-
-
# Returns the first record, or the first +n+ records, from the collection.
-
# If the collection is empty, the first form returns +nil+, and the second
-
# form returns an empty array.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
-
#
-
# person.pets.first(2)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>
-
# # ]
-
#
-
# another_person_without.pets # => []
-
# another_person_without.pets.first # => nil
-
# another_person_without.pets.first(3) # => []
-
2
def first(*args)
-
@association.first(*args)
-
end
-
-
# Same as +first+ except returns only the second record.
-
2
def second(*args)
-
@association.second(*args)
-
end
-
-
# Same as +first+ except returns only the third record.
-
2
def third(*args)
-
@association.third(*args)
-
end
-
-
# Same as +first+ except returns only the fourth record.
-
2
def fourth(*args)
-
@association.fourth(*args)
-
end
-
-
# Same as +first+ except returns only the fifth record.
-
2
def fifth(*args)
-
@association.fifth(*args)
-
end
-
-
# Same as +first+ except returns only the forty second record.
-
# Also known as accessing "the reddit".
-
2
def forty_two(*args)
-
@association.forty_two(*args)
-
end
-
-
# Returns the last record, or the last +n+ records, from the collection.
-
# If the collection is empty, the first form returns +nil+, and the second
-
# form returns an empty array.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
#
-
# person.pets.last(2)
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# another_person_without.pets # => []
-
# another_person_without.pets.last # => nil
-
# another_person_without.pets.last(3) # => []
-
2
def last(*args)
-
@association.last(*args)
-
end
-
-
2
def take(n = nil)
-
@association.take(n)
-
end
-
-
# Returns a new object of the collection type that has been instantiated
-
# with +attributes+ and linked to this object, but have not yet been saved.
-
# You can pass an array of attributes hashes, this will return an array
-
# with the new objects.
-
#
-
# class Person
-
# has_many :pets
-
# end
-
#
-
# person.pets.build
-
# # => #<Pet id: nil, name: nil, person_id: 1>
-
#
-
# person.pets.build(name: 'Fancy-Fancy')
-
# # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1>
-
#
-
# person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}])
-
# # => [
-
# # #<Pet id: nil, name: "Spook", person_id: 1>,
-
# # #<Pet id: nil, name: "Choo-Choo", person_id: 1>,
-
# # #<Pet id: nil, name: "Brain", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 5 # size of the collection
-
# person.pets.count # => 0 # count from database
-
2
def build(attributes = {}, &block)
-
@association.build(attributes, &block)
-
end
-
2
alias_method :new, :build
-
-
# Returns a new object of the collection type that has been instantiated with
-
# attributes, linked to this object and that has already been saved (if it
-
# passes the validations).
-
#
-
# class Person
-
# has_many :pets
-
# end
-
#
-
# person.pets.create(name: 'Fancy-Fancy')
-
# # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
-
#
-
# person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}])
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 3
-
# person.pets.count # => 3
-
#
-
# person.pets.find(1, 2, 3)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
2
def create(attributes = {}, &block)
-
@association.create(attributes, &block)
-
end
-
-
# Like +create+, except that if the record is invalid, raises an exception.
-
#
-
# class Person
-
# has_many :pets
-
# end
-
#
-
# class Pet
-
# validates :name, presence: true
-
# end
-
#
-
# person.pets.create!(name: nil)
-
# # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
-
2
def create!(attributes = {}, &block)
-
@association.create!(attributes, &block)
-
end
-
-
# Add one or more records to the collection by setting their foreign keys
-
# to the association's primary key. Since << flattens its argument list and
-
# inserts each record, +push+ and +concat+ behave identically. Returns +self+
-
# so method calls may be chained.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 0
-
# person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
-
# person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
-
# person.pets.size # => 3
-
#
-
# person.id # => 1
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
-
# person.pets.size # => 5
-
2
def concat(*records)
-
@association.concat(*records)
-
end
-
-
# Replaces this collection with +other_array+. This will perform a diff
-
# and delete/add only records that have changed.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
-
#
-
# other_pets = [Pet.new(name: 'Puff', group: 'celebrities']
-
#
-
# person.pets.replace(other_pets)
-
#
-
# person.pets
-
# # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
-
#
-
# If the supplied array has an incorrect association type, it raises
-
# an <tt>ActiveRecord::AssociationTypeMismatch</tt> error:
-
#
-
# person.pets.replace(["doo", "ggie", "gaga"])
-
# # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String
-
2
def replace(other_array)
-
@association.replace(other_array)
-
end
-
-
# Deletes all the records from the collection according to the strategy
-
# specified by the +:dependent+ option. If no +:dependent+ option is given,
-
# then it will follow the default strategy.
-
#
-
# For +has_many :through+ associations, the default deletion strategy is
-
# +:delete_all+.
-
#
-
# For +has_many+ associations, the default deletion strategy is +:nullify+.
-
# This sets the foreign keys to +NULL+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets # dependent: :nullify option by default
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete_all
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(1, 2, 3)
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>,
-
# # #<Pet id: 2, name: "Spook", person_id: nil>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
-
# # ]
-
#
-
# Both +has_many+ and +has_many :through+ dependencies default to the
-
# +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
-
# Records are not instantiated and callbacks will not be fired.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :destroy
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete_all
-
#
-
# Pet.find(1, 2, 3)
-
# # => ActiveRecord::RecordNotFound
-
#
-
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
-
# *without* calling their +destroy+ method.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :delete_all
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete_all
-
#
-
# Pet.find(1, 2, 3)
-
# # => ActiveRecord::RecordNotFound
-
2
def delete_all(dependent = nil)
-
@association.delete_all(dependent)
-
end
-
-
# Deletes the records of the collection directly from the database
-
# ignoring the +:dependent+ option. Records are instantiated and it
-
# invokes +before_remove+, +after_remove+ , +before_destroy+ and
-
# +after_destroy+ callbacks.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy_all
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(1) # => Couldn't find Pet with id=1
-
2
def destroy_all
-
@association.destroy_all
-
end
-
-
# Deletes the +records+ supplied from the collection according to the strategy
-
# specified by the +:dependent+ option. If no +:dependent+ option is given,
-
# then it will follow the default strategy. Returns an array with the
-
# deleted records.
-
#
-
# For +has_many :through+ associations, the default deletion strategy is
-
# +:delete_all+.
-
#
-
# For +has_many+ associations, the default deletion strategy is +:nullify+.
-
# This sets the foreign keys to +NULL+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets # dependent: :nullify option by default
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete(Pet.find(1))
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# Pet.find(1)
-
# # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>
-
#
-
# If it is set to <tt>:destroy</tt> all the +records+ are removed by calling
-
# their +destroy+ method. See +destroy+ for more information.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :destroy
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete(Pet.find(1), Pet.find(3))
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 1
-
# person.pets
-
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
-
#
-
# Pet.find(1, 3)
-
# # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
-
#
-
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
-
# *without* calling their +destroy+ method.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets, dependent: :delete_all
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete(Pet.find(1))
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# Pet.find(1)
-
# # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1
-
#
-
# You can pass +Fixnum+ or +String+ values, it finds the records
-
# responding to the +id+ and executes delete on them.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.delete("1")
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.delete(2, 3)
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
2
def delete(*records)
-
@association.delete(*records)
-
end
-
-
# Destroys the +records+ supplied and removes them from the collection.
-
# This method will _always_ remove record from the database ignoring
-
# the +:dependent+ option. Returns an array with the removed records.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy(Pet.find(1))
-
# # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>]
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy(Pet.find(2), Pet.find(3))
-
# # => [
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
-
#
-
# You can pass +Fixnum+ or +String+ values, it finds the records
-
# responding to the +id+ and then deletes them from the database.
-
#
-
# person.pets.size # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy("4")
-
# # => #<Pet id: 4, name: "Benny", person_id: 1>
-
#
-
# person.pets.size # => 2
-
# person.pets
-
# # => [
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# person.pets.destroy(5, 6)
-
# # => [
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 0
-
# person.pets # => []
-
#
-
# Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
-
2
def destroy(*records)
-
@association.destroy(*records)
-
end
-
-
# Specifies whether the records should be unique or not.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.select(:name)
-
# # => [
-
# # #<Pet name: "Fancy-Fancy">,
-
# # #<Pet name: "Fancy-Fancy">
-
# # ]
-
#
-
# person.pets.select(:name).distinct
-
# # => [#<Pet name: "Fancy-Fancy">]
-
2
def distinct
-
@association.distinct
-
end
-
2
alias uniq distinct
-
-
# Count all records using SQL.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.count # => 3
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
2
def count(column_name = nil, options = {})
-
# TODO: Remove options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
@association.count(column_name, options)
-
end
-
-
# Returns the size of the collection. If the collection hasn't been loaded,
-
# it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
-
#
-
# If the collection has been already loaded +size+ and +length+ are
-
# equivalent. If not and you are going to need the records anyway
-
# +length+ will take one less query. Otherwise +size+ is more efficient.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 3
-
# # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1
-
#
-
# person.pets # This will execute a SELECT * FROM query
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
#
-
# person.pets.size # => 3
-
# # Because the collection is already loaded, this will behave like
-
# # collection.size and no SQL count query is executed.
-
2
def size
-
@association.size
-
end
-
-
# Returns the size of the collection calling +size+ on the target.
-
# If the collection has been already loaded, +length+ and +size+ are
-
# equivalent. If not and you are going to need the records anyway this
-
# method will take one less query. Otherwise +size+ is more efficient.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.length # => 3
-
# # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1
-
#
-
# # Because the collection is loaded, you can
-
# # call the collection with no additional queries:
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
2
def length
-
@association.length
-
end
-
-
# Returns +true+ if the collection is empty. If the collection has been
-
# loaded it is equivalent
-
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
-
# it is equivalent to <tt>collection.exists?</tt>. If the collection has
-
# not already been loaded and you are going to fetch the records anyway it
-
# is better to check <tt>collection.length.zero?</tt>.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.count # => 1
-
# person.pets.empty? # => false
-
#
-
# person.pets.delete_all
-
#
-
# person.pets.count # => 0
-
# person.pets.empty? # => true
-
2
def empty?
-
@association.empty?
-
end
-
-
# Returns +true+ if the collection is not empty.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.count # => 0
-
# person.pets.any? # => false
-
#
-
# person.pets << Pet.new(name: 'Snoop')
-
# person.pets.count # => 0
-
# person.pets.any? # => true
-
#
-
# You can also pass a +block+ to define criteria. The behavior
-
# is the same, it returns true if the collection based on the
-
# criteria is not empty.
-
#
-
# person.pets
-
# # => [#<Pet name: "Snoop", group: "dogs">]
-
#
-
# person.pets.any? do |pet|
-
# pet.group == 'cats'
-
# end
-
# # => false
-
#
-
# person.pets.any? do |pet|
-
# pet.group == 'dogs'
-
# end
-
# # => true
-
2
def any?(&block)
-
@association.any?(&block)
-
end
-
-
# Returns true if the collection has more than one record.
-
# Equivalent to <tt>collection.size > 1</tt>.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.count # => 1
-
# person.pets.many? # => false
-
#
-
# person.pets << Pet.new(name: 'Snoopy')
-
# person.pets.count # => 2
-
# person.pets.many? # => true
-
#
-
# You can also pass a +block+ to define criteria. The
-
# behavior is the same, it returns true if the collection
-
# based on the criteria has more than one record.
-
#
-
# person.pets
-
# # => [
-
# # #<Pet name: "Gorby", group: "cats">,
-
# # #<Pet name: "Puff", group: "cats">,
-
# # #<Pet name: "Snoop", group: "dogs">
-
# # ]
-
#
-
# person.pets.many? do |pet|
-
# pet.group == 'dogs'
-
# end
-
# # => false
-
#
-
# person.pets.many? do |pet|
-
# pet.group == 'cats'
-
# end
-
# # => true
-
2
def many?(&block)
-
@association.many?(&block)
-
end
-
-
# Returns +true+ if the given +record+ is present in the collection.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets # => [#<Pet id: 20, name: "Snoop">]
-
#
-
# person.pets.include?(Pet.find(20)) # => true
-
# person.pets.include?(Pet.find(21)) # => false
-
2
def include?(record)
-
!!@association.include?(record)
-
end
-
-
2
def arel
-
scope.arel
-
end
-
-
2
def proxy_association
-
@association
-
end
-
-
# We don't want this object to be put on the scoping stack, because
-
# that could create an infinite loop where we call an @association
-
# method, which gets the current scope, which is this object, which
-
# delegates to @association, and so on.
-
2
def scoping
-
@association.scope.scoping { yield }
-
end
-
-
# Returns a <tt>Relation</tt> object for the records in this association
-
2
def scope
-
3
@association.scope
-
end
-
2
alias spawn scope
-
-
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
-
# contain the same number of elements and if each element is equal
-
# to the corresponding element in the +other+ array, otherwise returns
-
# +false+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>
-
# # ]
-
#
-
# other = person.pets.to_ary
-
#
-
# person.pets == other
-
# # => true
-
#
-
# other = [Pet.new(id: 1), Pet.new(id: 2)]
-
#
-
# person.pets == other
-
# # => false
-
2
def ==(other)
-
load_target == other
-
end
-
-
# Returns a new array of objects from the collection. If the collection
-
# hasn't been loaded, it fetches the records from the database.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# other_pets = person.pets.to_ary
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
#
-
# other_pets.replace([Pet.new(name: 'BooGoo')])
-
#
-
# other_pets
-
# # => [#<Pet id: nil, name: "BooGoo", person_id: 1>]
-
#
-
# person.pets
-
# # This is not affected by replace
-
# # => [
-
# # #<Pet id: 4, name: "Benny", person_id: 1>,
-
# # #<Pet id: 5, name: "Brain", person_id: 1>,
-
# # #<Pet id: 6, name: "Boss", person_id: 1>
-
# # ]
-
2
def to_ary
-
load_target.dup
-
end
-
2
alias_method :to_a, :to_ary
-
-
# Adds one or more +records+ to the collection by setting their foreign keys
-
# to the association's primary key. Returns +self+, so several appends may be
-
# chained together.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets.size # => 0
-
# person.pets << Pet.new(name: 'Fancy-Fancy')
-
# person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
-
# person.pets.size # => 3
-
#
-
# person.id # => 1
-
# person.pets
-
# # => [
-
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
-
# # #<Pet id: 2, name: "Spook", person_id: 1>,
-
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
-
# # ]
-
2
def <<(*records)
-
proxy_association.concat(records) && self
-
end
-
2
alias_method :push, :<<
-
2
alias_method :append, :<<
-
-
2
def prepend(*args)
-
raise NoMethodError, "prepend on association is not defined. Please use << or append"
-
end
-
-
# Equivalent to +delete_all+. The difference is that returns +self+, instead
-
# of an array with the deleted objects, so methods can be chained. See
-
# +delete_all+ for more information.
-
2
def clear
-
delete_all
-
self
-
end
-
-
# Reloads the collection from the database. Returns +self+.
-
# Equivalent to <tt>collection(true)</tt>.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets # uses the pets cache
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets.reload # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets(true) # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
2
def reload
-
proxy_association.reload
-
self
-
end
-
-
# Unloads the association. Returns +self+.
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :pets
-
# end
-
#
-
# person.pets # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets # uses the pets cache
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
#
-
# person.pets.reset # clears the pets cache
-
#
-
# person.pets # fetches pets from the database
-
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
-
2
def reset
-
proxy_association.reset
-
proxy_association.reset_scope
-
self
-
end
-
end
-
end
-
end
-
1
module ActiveRecord::Associations
-
1
module ForeignAssociation
-
1
def foreign_key_present?
-
if reflection.klass.primary_key
-
owner.attribute_present?(reflection.active_record_primary_key)
-
else
-
false
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
# = Active Record Has Many Association
-
1
module Associations
-
# This is the proxy that handles a has many association.
-
#
-
# If the association has a <tt>:through</tt> option further specialization
-
# is provided by its child HasManyThroughAssociation.
-
1
class HasManyAssociation < CollectionAssociation #:nodoc:
-
1
include ForeignAssociation
-
-
1
def handle_dependency
-
case options[:dependent]
-
when :restrict_with_exception
-
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
-
-
when :restrict_with_error
-
unless empty?
-
record = klass.human_attribute_name(reflection.name).downcase
-
owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
-
false
-
end
-
-
else
-
if options[:dependent] == :destroy
-
# No point in executing the counter update since we're going to destroy the parent anyway
-
load_target.each { |t| t.destroyed_by_association = reflection }
-
destroy_all
-
else
-
delete_all
-
end
-
end
-
end
-
-
1
def insert_record(record, validate = true, raise = false)
-
set_owner_attributes(record)
-
set_inverse_instance(record)
-
-
if raise
-
record.save!(:validate => validate)
-
else
-
record.save(:validate => validate)
-
end
-
end
-
-
1
def empty?
-
if has_cached_counter?
-
size.zero?
-
else
-
super
-
end
-
end
-
-
1
private
-
-
# Returns the number of records in this collection.
-
#
-
# If the association has a counter cache it gets that value. Otherwise
-
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
-
# there's one. Some configuration options like :group make it impossible
-
# to do an SQL count, in those cases the array count will be used.
-
#
-
# That does not depend on whether the collection has already been loaded
-
# or not. The +size+ method is the one that takes the loaded flag into
-
# account and delegates to +count_records+ if needed.
-
#
-
# If the collection is empty the target is set to an empty array and
-
# the loaded flag is set to true as well.
-
1
def count_records
-
count = if has_cached_counter?
-
owner._read_attribute cached_counter_attribute_name
-
else
-
scope.count
-
end
-
-
# If there's nothing in the database and @target has no new records
-
# we are certain the current target is an empty array. This is a
-
# documented side-effect of the method that may avoid an extra SELECT.
-
@target ||= [] and loaded! if count == 0
-
-
[association_scope.limit_value, count].compact.min
-
end
-
-
-
# Returns whether a counter cache should be used for this association.
-
#
-
# The counter_cache option must be given on either the owner or inverse
-
# association, and the column must be present on the owner.
-
1
def has_cached_counter?(reflection = reflection())
-
if reflection.options[:counter_cache] || (inverse = inverse_which_updates_counter_cache(reflection)) && inverse.options[:counter_cache]
-
owner.attribute_present?(cached_counter_attribute_name(reflection))
-
end
-
end
-
-
1
def cached_counter_attribute_name(reflection = reflection())
-
if reflection.options[:counter_cache]
-
reflection.options[:counter_cache].to_s
-
else
-
"#{reflection.name}_count"
-
end
-
end
-
-
1
def update_counter(difference, reflection = reflection())
-
update_counter_in_database(difference, reflection)
-
update_counter_in_memory(difference, reflection)
-
end
-
-
1
def update_counter_in_database(difference, reflection = reflection())
-
if has_cached_counter?(reflection)
-
counter = cached_counter_attribute_name(reflection)
-
owner.class.update_counters(owner.id, counter => difference)
-
end
-
end
-
-
1
def update_counter_in_memory(difference, reflection = reflection())
-
if counter_must_be_updated_by_has_many?(reflection)
-
counter = cached_counter_attribute_name(reflection)
-
owner[counter] += difference
-
owner.send(:clear_attribute_changes, counter) # eww
-
end
-
end
-
-
# This shit is nasty. We need to avoid the following situation:
-
#
-
# * An associated record is deleted via record.destroy
-
# * Hence the callbacks run, and they find a belongs_to on the record with a
-
# :counter_cache options which points back at our owner. So they update the
-
# counter cache.
-
# * In which case, we must make sure to *not* update the counter cache, or else
-
# it will be decremented twice.
-
#
-
# Hence this method.
-
1
def inverse_which_updates_counter_cache(reflection = reflection())
-
counter_name = cached_counter_attribute_name(reflection)
-
inverse_which_updates_counter_named(counter_name, reflection)
-
end
-
1
alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
-
-
1
def inverse_which_updates_counter_named(counter_name, reflection)
-
reflection.klass._reflections.values.find { |inverse_reflection|
-
inverse_reflection.belongs_to? &&
-
inverse_reflection.counter_cache_column == counter_name
-
}
-
end
-
1
alias inverse_updates_counter_named? inverse_which_updates_counter_named
-
-
1
def inverse_updates_counter_in_memory?(reflection)
-
inverse = inverse_which_updates_counter_cache(reflection)
-
inverse && inverse == reflection.inverse_of
-
end
-
-
1
def counter_must_be_updated_by_has_many?(reflection)
-
!inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection)
-
end
-
-
1
def delete_count(method, scope)
-
if method == :delete_all
-
scope.delete_all
-
else
-
scope.update_all(reflection.foreign_key => nil)
-
end
-
end
-
-
1
def delete_or_nullify_all_records(method)
-
count = delete_count(method, self.scope)
-
update_counter(-count)
-
end
-
-
# Deletes the records according to the <tt>:dependent</tt> option.
-
1
def delete_records(records, method)
-
if method == :destroy
-
records.each(&:destroy!)
-
update_counter(-records.length) unless inverse_updates_counter_cache?
-
else
-
scope = self.scope.where(reflection.klass.primary_key => records)
-
update_counter(-delete_count(method, scope))
-
end
-
end
-
-
1
def concat_records(records, *)
-
update_counter_if_success(super, records.length)
-
end
-
-
1
def _create_record(attributes, *)
-
if attributes.is_a?(Array)
-
super
-
else
-
update_counter_if_success(super, 1)
-
end
-
end
-
-
1
def update_counter_if_success(saved_successfully, difference)
-
if saved_successfully
-
update_counter_in_memory(difference)
-
end
-
saved_successfully
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Associations
-
1
class JoinDependency # :nodoc:
-
1
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
-
1
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
-
-
1
class Aliases # :nodoc:
-
1
def initialize(tables)
-
@tables = tables
-
@alias_cache = tables.each_with_object({}) { |table,h|
-
h[table.node] = table.columns.each_with_object({}) { |column,i|
-
i[column.name] = column.alias
-
}
-
}
-
@name_and_alias_cache = tables.each_with_object({}) { |table,h|
-
h[table.node] = table.columns.map { |column|
-
[column.name, column.alias]
-
}
-
}
-
end
-
-
1
def columns
-
@tables.flat_map { |t| t.column_aliases }
-
end
-
-
# An array of [column_name, alias] pairs for the table
-
1
def column_aliases(node)
-
@name_and_alias_cache[node]
-
end
-
-
1
def column_alias(node, column)
-
@alias_cache[node][column]
-
end
-
-
1
class Table < Struct.new(:node, :columns)
-
1
def table
-
Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
-
end
-
-
1
def column_aliases
-
t = table
-
columns.map { |column| t[column.name].as Arel.sql column.alias }
-
end
-
end
-
1
Column = Struct.new(:name, :alias)
-
end
-
-
1
attr_reader :alias_tracker, :base_klass, :join_root
-
-
1
def self.make_tree(associations)
-
30
hash = {}
-
30
walk_tree associations, hash
-
30
hash
-
end
-
-
1
def self.walk_tree(associations, hash)
-
30
case associations
-
when Symbol, String
-
hash[associations.to_sym] ||= {}
-
when Array
-
30
associations.each do |assoc|
-
walk_tree assoc, hash
-
end
-
when Hash
-
associations.each do |k,v|
-
cache = hash[k] ||= {}
-
walk_tree v, cache
-
end
-
else
-
raise ConfigurationError, associations.inspect
-
end
-
end
-
-
# base is the base class on which operation is taking place.
-
# associations is the list of associations which are joined using hash, symbol or array.
-
# joins is the list of all string join commands and arel nodes.
-
#
-
# Example :
-
#
-
# class Physician < ActiveRecord::Base
-
# has_many :appointments
-
# has_many :patients, through: :appointments
-
# end
-
#
-
# If I execute `@physician.patients.to_a` then
-
# base # => Physician
-
# associations # => []
-
# joins # => [#<Arel::Nodes::InnerJoin: ...]
-
#
-
# However if I execute `Physician.joins(:appointments).to_a` then
-
# base # => Physician
-
# associations # => [:appointments]
-
# joins # => []
-
#
-
1
def initialize(base, associations, joins)
-
30
@alias_tracker = AliasTracker.create(base.connection, joins)
-
30
@alias_tracker.aliased_table_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1
-
30
tree = self.class.make_tree associations
-
30
@join_root = JoinBase.new base, build(tree, base)
-
30
@join_root.children.each { |child| construct_tables! @join_root, child }
-
end
-
-
1
def reflections
-
15
join_root.drop(1).map!(&:reflection)
-
end
-
-
1
def join_constraints(outer_joins)
-
15
joins = join_root.children.flat_map { |child|
-
make_inner_joins join_root, child
-
}
-
-
15
joins.concat outer_joins.flat_map { |oj|
-
15
if join_root.match? oj.join_root
-
15
walk join_root, oj.join_root
-
else
-
oj.join_root.children.flat_map { |child|
-
make_outer_joins oj.join_root, child
-
}
-
end
-
}
-
end
-
-
1
def aliases
-
Aliases.new join_root.each_with_index.map { |join_part,i|
-
columns = join_part.column_names.each_with_index.map { |column_name,j|
-
Aliases::Column.new column_name, "t#{i}_r#{j}"
-
}
-
Aliases::Table.new(join_part, columns)
-
}
-
end
-
-
1
def instantiate(result_set, aliases)
-
primary_key = aliases.column_alias(join_root, join_root.primary_key)
-
-
seen = Hash.new { |h,parent_klass|
-
h[parent_klass] = Hash.new { |i,parent_id|
-
i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
-
}
-
}
-
-
model_cache = Hash.new { |h,klass| h[klass] = {} }
-
parents = model_cache[join_root]
-
column_aliases = aliases.column_aliases join_root
-
-
message_bus = ActiveSupport::Notifications.instrumenter
-
-
payload = {
-
record_count: result_set.length,
-
class_name: join_root.base_klass.name
-
}
-
-
message_bus.instrument('instantiation.active_record', payload) do
-
result_set.each { |row_hash|
-
parent_key = primary_key ? row_hash[primary_key] : row_hash
-
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases)
-
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
-
}
-
end
-
-
parents.values
-
end
-
-
1
private
-
-
1
def make_constraints(parent, child, tables, join_type)
-
chain = child.reflection.chain
-
foreign_table = parent.table
-
foreign_klass = parent.base_klass
-
child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
-
end
-
-
1
def make_outer_joins(parent, child)
-
tables = table_aliases_for(parent, child)
-
join_type = Arel::Nodes::OuterJoin
-
info = make_constraints parent, child, tables, join_type
-
-
[info] + child.children.flat_map { |c| make_outer_joins(child, c) }
-
end
-
-
1
def make_inner_joins(parent, child)
-
tables = child.tables
-
join_type = Arel::Nodes::InnerJoin
-
info = make_constraints parent, child, tables, join_type
-
-
[info] + child.children.flat_map { |c| make_inner_joins(child, c) }
-
end
-
-
1
def table_aliases_for(parent, node)
-
node.reflection.chain.map { |reflection|
-
alias_tracker.aliased_table_for(
-
reflection.table_name,
-
table_alias_for(reflection, parent, reflection != node.reflection)
-
)
-
}
-
end
-
-
1
def construct_tables!(parent, node)
-
node.tables = table_aliases_for(parent, node)
-
node.children.each { |child| construct_tables! node, child }
-
end
-
-
1
def table_alias_for(reflection, parent, join)
-
name = "#{reflection.plural_name}_#{parent.table_name}"
-
name << "_join" if join
-
name
-
end
-
-
1
def walk(left, right)
-
15
intersection, missing = right.children.map { |node1|
-
[left.children.find { |node2| node1.match? node2 }, node1]
-
}.partition(&:first)
-
-
15
ojs = missing.flat_map { |_,n| make_outer_joins left, n }
-
15
intersection.flat_map { |l,r| walk l, r }.concat ojs
-
end
-
-
1
def find_reflection(klass, name)
-
klass._reflect_on_association(name) or
-
raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
-
end
-
-
1
def build(associations, base_klass)
-
30
associations.map do |name, right|
-
reflection = find_reflection base_klass, name
-
reflection.check_validity!
-
reflection.check_eager_loadable!
-
-
if reflection.polymorphic?
-
raise EagerLoadPolymorphicError.new(reflection)
-
end
-
-
JoinAssociation.new reflection, build(right, reflection.klass)
-
end
-
end
-
-
1
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
-
return if ar_parent.nil?
-
primary_id = ar_parent.id
-
-
parent.children.each do |node|
-
if node.reflection.collection?
-
other = ar_parent.association(node.reflection.name)
-
other.loaded!
-
else
-
if ar_parent.association_cache.key?(node.reflection.name)
-
model = ar_parent.association(node.reflection.name).target
-
construct(model, node, row, rs, seen, model_cache, aliases)
-
next
-
end
-
end
-
-
key = aliases.column_alias(node, node.primary_key)
-
id = row[key]
-
if id.nil?
-
nil_association = ar_parent.association(node.reflection.name)
-
nil_association.loaded!
-
next
-
end
-
-
model = seen[parent.base_klass][primary_id][node.base_klass][id]
-
-
if model
-
construct(model, node, row, rs, seen, model_cache, aliases)
-
else
-
model = construct_model(ar_parent, node, row, model_cache, id, aliases)
-
seen[parent.base_klass][primary_id][node.base_klass][id] = model
-
construct(model, node, row, rs, seen, model_cache, aliases)
-
end
-
end
-
end
-
-
1
def construct_model(record, node, row, model_cache, id, aliases)
-
model = model_cache[node][id] ||= node.instantiate(row,
-
aliases.column_aliases(node))
-
other = record.association(node.reflection.name)
-
-
if node.reflection.collection?
-
other.target.push(model)
-
else
-
other.target = model
-
end
-
-
other.set_inverse_instance(model)
-
model
-
end
-
end
-
end
-
end
-
1
require 'active_record/associations/join_dependency/join_part'
-
-
1
module ActiveRecord
-
1
module Associations
-
1
class JoinDependency # :nodoc:
-
1
class JoinBase < JoinPart # :nodoc:
-
1
def match?(other)
-
15
return true if self == other
-
15
super && base_klass == other.base_klass
-
end
-
-
1
def table
-
base_klass.arel_table
-
end
-
-
1
def aliased_table_name
-
base_klass.table_name
-
end
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Associations
-
1
class JoinDependency # :nodoc:
-
# A JoinPart represents a part of a JoinDependency. It is inherited
-
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
-
# everything else is being joined onto. A JoinAssociation represents an association which
-
# is joining to the base. A JoinAssociation may result in more than one actual join
-
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
-
# two; one for the join table and one for the target table).
-
1
class JoinPart # :nodoc:
-
1
include Enumerable
-
-
# The Active Record class which this join part is associated 'about'; for a JoinBase
-
# this is the actual base model, for a JoinAssociation this is the target model of the
-
# association.
-
1
attr_reader :base_klass, :children
-
-
1
delegate :table_name, :column_names, :primary_key, :to => :base_klass
-
-
1
def initialize(base_klass, children)
-
30
@base_klass = base_klass
-
30
@children = children
-
end
-
-
1
def name
-
reflection.name
-
end
-
-
1
def match?(other)
-
15
self.class == other.class
-
end
-
-
1
def each(&block)
-
15
yield self
-
15
children.each { |child| child.each(&block) }
-
end
-
-
# An Arel::Table for the active_record
-
1
def table
-
raise NotImplementedError
-
end
-
-
# The alias for the active_record's table
-
1
def aliased_table_name
-
raise NotImplementedError
-
end
-
-
1
def extract_record(row, column_names_with_alias)
-
# This code is performance critical as it is called per row.
-
# see: https://github.com/rails/rails/pull/12185
-
hash = {}
-
-
index = 0
-
length = column_names_with_alias.length
-
-
while index < length
-
column_name, alias_name = column_names_with_alias[index]
-
hash[column_name] = row[alias_name]
-
index += 1
-
end
-
-
hash
-
end
-
-
1
def instantiate(row, aliases)
-
base_klass.instantiate(extract_record(row, aliases))
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Associations
-
# Implements the details of eager loading of Active Record associations.
-
#
-
# Suppose that you have the following two Active Record models:
-
#
-
# class Author < ActiveRecord::Base
-
# # columns: name, age
-
# has_many :books
-
# end
-
#
-
# class Book < ActiveRecord::Base
-
# # columns: title, sales, author_id
-
# end
-
#
-
# When you load an author with all associated books Active Record will make
-
# multiple queries like this:
-
#
-
# Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a
-
#
-
# => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
-
# => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
-
#
-
# Active Record saves the ids of the records from the first query to use in
-
# the second. Depending on the number of associations involved there can be
-
# arbitrarily many SQL queries made.
-
#
-
# However, if there is a WHERE clause that spans across tables Active
-
# Record will fall back to a slightly more resource-intensive single query:
-
#
-
# Author.includes(:books).where(books: {title: 'Illiad'}).to_a
-
# => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
-
# `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
-
# FROM `authors`
-
# LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
-
# WHERE `books`.`title` = 'Illiad'
-
#
-
# This could result in many rows that contain redundant data and it performs poorly at scale
-
# and is therefore only used when necessary.
-
#
-
2
class Preloader #:nodoc:
-
2
extend ActiveSupport::Autoload
-
-
2
eager_autoload do
-
2
autoload :Association, 'active_record/associations/preloader/association'
-
2
autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
-
2
autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
-
2
autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
-
-
2
autoload :HasMany, 'active_record/associations/preloader/has_many'
-
2
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
-
2
autoload :HasOne, 'active_record/associations/preloader/has_one'
-
2
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
-
2
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
-
end
-
-
# Eager loads the named associations for the given Active Record record(s).
-
#
-
# In this description, 'association name' shall refer to the name passed
-
# to an association creation method. For example, a model that specifies
-
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
-
# names +:author+ and +:buyers+.
-
#
-
# == Parameters
-
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
-
# i.e. +records+ itself may also contain arrays of records. In any case,
-
# +preload_associations+ will preload the all associations records by
-
# flattening +records+.
-
#
-
# +associations+ specifies one or more associations that you want to
-
# preload. It may be:
-
# - a Symbol or a String which specifies a single association name. For
-
# example, specifying +:books+ allows this method to preload all books
-
# for an Author.
-
# - an Array which specifies multiple association names. This array
-
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
-
# allows this method to preload an author's avatar as well as all of his
-
# books.
-
# - a Hash which specifies multiple association names, as well as
-
# association names for the to-be-preloaded association objects. For
-
# example, specifying <tt>{ author: :avatar }</tt> will preload a
-
# book's author, as well as that author's avatar.
-
#
-
# +:associations+ has the same format as the +:include+ option for
-
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
-
#
-
# :books
-
# [ :books, :author ]
-
# { author: :avatar }
-
# [ :books, { author: :avatar } ]
-
-
2
NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
-
-
2
def preload(records, associations, preload_scope = nil)
-
records = Array.wrap(records).compact.uniq
-
associations = Array.wrap(associations)
-
preload_scope = preload_scope || NULL_RELATION
-
-
if records.empty?
-
[]
-
else
-
associations.flat_map { |association|
-
preloaders_on association, records, preload_scope
-
}
-
end
-
end
-
-
2
private
-
-
2
def preloaders_on(association, records, scope)
-
case association
-
when Hash
-
preloaders_for_hash(association, records, scope)
-
when Symbol
-
preloaders_for_one(association, records, scope)
-
when String
-
preloaders_for_one(association.to_sym, records, scope)
-
else
-
raise ArgumentError, "#{association.inspect} was not recognised for preload"
-
end
-
end
-
-
2
def preloaders_for_hash(association, records, scope)
-
association.flat_map { |parent, child|
-
loaders = preloaders_for_one parent, records, scope
-
-
recs = loaders.flat_map(&:preloaded_records).uniq
-
loaders.concat Array.wrap(child).flat_map { |assoc|
-
preloaders_on assoc, recs, scope
-
}
-
loaders
-
}
-
end
-
-
# Not all records have the same class, so group then preload group on the reflection
-
# itself so that if various subclass share the same association then we do not split
-
# them unnecessarily
-
#
-
# Additionally, polymorphic belongs_to associations can have multiple associated
-
# classes, depending on the polymorphic_type field. So we group by the classes as
-
# well.
-
2
def preloaders_for_one(association, records, scope)
-
grouped_records(association, records).flat_map do |reflection, klasses|
-
klasses.map do |rhs_klass, rs|
-
loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
-
loader.run self
-
loader
-
end
-
end
-
end
-
-
2
def grouped_records(association, records)
-
h = {}
-
records.each do |record|
-
next unless record
-
assoc = record.association(association)
-
klasses = h[assoc.reflection] ||= {}
-
(klasses[assoc.klass] ||= []) << record
-
end
-
h
-
end
-
-
2
class AlreadyLoaded # :nodoc:
-
2
attr_reader :owners, :reflection
-
-
2
def initialize(klass, owners, reflection, preload_scope)
-
@owners = owners
-
@reflection = reflection
-
end
-
-
2
def run(preloader); end
-
-
2
def preloaded_records
-
owners.flat_map { |owner| owner.association(reflection.name).target }
-
end
-
end
-
-
2
class NullPreloader # :nodoc:
-
2
def self.new(klass, owners, reflection, preload_scope); self; end
-
2
def self.run(preloader); end
-
2
def self.preloaded_records; []; end
-
end
-
-
2
def preloader_for(reflection, owners, rhs_klass)
-
return NullPreloader unless rhs_klass
-
-
if owners.first.association(reflection.name).loaded?
-
return AlreadyLoaded
-
end
-
reflection.check_preloadable!
-
-
case reflection.macro
-
when :has_many
-
reflection.options[:through] ? HasManyThrough : HasMany
-
when :has_one
-
reflection.options[:through] ? HasOneThrough : HasOne
-
when :belongs_to
-
BelongsTo
-
end
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Associations
-
1
class SingularAssociation < Association #:nodoc:
-
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
-
1
def reader(force_reload = false)
-
4
if force_reload && klass
-
klass.uncached { reload }
-
elsif !loaded? || stale_target?
-
4
reload
-
end
-
-
4
target
-
end
-
-
# Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
-
1
def writer(record)
-
replace(record)
-
end
-
-
1
def create(attributes = {}, &block)
-
_create_record(attributes, &block)
-
end
-
-
1
def create!(attributes = {}, &block)
-
_create_record(attributes, true, &block)
-
end
-
-
1
def build(attributes = {})
-
record = build_record(attributes)
-
yield(record) if block_given?
-
set_new_record(record)
-
record
-
end
-
-
1
private
-
-
1
def create_scope
-
scope.scope_for_create.stringify_keys.except(klass.primary_key)
-
end
-
-
1
def get_records
-
3
return scope.limit(1).to_a if skip_statement_cache?
-
-
3
conn = klass.connection
-
3
sc = reflection.association_scope_cache(conn, owner) do
-
2
StatementCache.create(conn) { |params|
-
4
as = AssociationScope.create { params.bind }
-
2
target_scope.merge(as.scope(self, conn)).limit(1)
-
}
-
end
-
-
3
binds = AssociationScope.get_bind_values(owner, reflection.chain)
-
3
sc.execute binds, klass, klass.connection
-
end
-
-
1
def find_target
-
3
if record = get_records.first
-
1
set_inverse_instance record
-
end
-
end
-
-
1
def replace(record)
-
raise NotImplementedError, "Subclasses must implement a replace(record) method"
-
end
-
-
1
def set_new_record(record)
-
replace(record)
-
end
-
-
1
def _create_record(attributes, raise_error = false)
-
record = build_record(attributes)
-
yield(record) if block_given?
-
saved = record.save
-
set_new_record(record)
-
raise RecordInvalid.new(record) if !saved && raise_error
-
record
-
end
-
end
-
end
-
end
-
2
require 'active_model/forbidden_attributes_protection'
-
-
2
module ActiveRecord
-
2
module AttributeAssignment
-
2
extend ActiveSupport::Concern
-
2
include ActiveModel::ForbiddenAttributesProtection
-
-
# Allows you to set all the attributes by passing in a hash of attributes with
-
# keys matching the attribute names (which again matches the column names).
-
#
-
# If the passed hash responds to <tt>permitted?</tt> method and the return value
-
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
-
# exception is raised.
-
#
-
# cat = Cat.new(name: "Gorby", status: "yawning")
-
# cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
-
# cat.assign_attributes(status: "sleeping")
-
# cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
-
#
-
# New attributes will be persisted in the database when the object is saved.
-
#
-
# Aliased to <tt>attributes=</tt>.
-
2
def assign_attributes(new_attributes)
-
18
if !new_attributes.respond_to?(:stringify_keys)
-
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
-
end
-
18
return if new_attributes.blank?
-
-
18
attributes = new_attributes.stringify_keys
-
18
multi_parameter_attributes = []
-
18
nested_parameter_attributes = []
-
-
18
attributes = sanitize_for_mass_assignment(attributes)
-
-
18
attributes.each do |k, v|
-
46
if k.include?("(")
-
multi_parameter_attributes << [ k, v ]
-
elsif v.is_a?(Hash)
-
nested_parameter_attributes << [ k, v ]
-
else
-
46
_assign_attribute(k, v)
-
end
-
end
-
-
18
assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
-
18
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
-
end
-
-
2
alias attributes= assign_attributes
-
-
2
private
-
-
2
def _assign_attribute(k, v)
-
46
public_send("#{k}=", v)
-
rescue NoMethodError, NameError
-
if respond_to?("#{k}=")
-
raise
-
else
-
raise UnknownAttributeError.new(self, k)
-
end
-
end
-
-
# Assign any deferred nested attributes after the base attributes have been set.
-
2
def assign_nested_parameter_attributes(pairs)
-
pairs.each { |k, v| _assign_attribute(k, v) }
-
end
-
-
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
-
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
-
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
-
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
-
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and
-
# f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
-
2
def assign_multiparameter_attributes(pairs)
-
execute_callstack_for_multiparameter_attributes(
-
extract_callstack_for_multiparameter_attributes(pairs)
-
)
-
end
-
-
2
def execute_callstack_for_multiparameter_attributes(callstack)
-
errors = []
-
callstack.each do |name, values_with_empty_parameters|
-
begin
-
send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
-
rescue => ex
-
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
-
end
-
end
-
unless errors.empty?
-
error_descriptions = errors.map { |ex| ex.message }.join(",")
-
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
-
end
-
end
-
-
2
def extract_callstack_for_multiparameter_attributes(pairs)
-
attributes = {}
-
-
pairs.each do |(multiparameter_name, value)|
-
attribute_name = multiparameter_name.split("(").first
-
attributes[attribute_name] ||= {}
-
-
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
-
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
-
end
-
-
attributes
-
end
-
-
2
def type_cast_attribute_value(multiparameter_name, value)
-
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
-
end
-
-
2
def find_parameter_position(multiparameter_name)
-
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
-
end
-
-
2
class MultiparameterAttribute #:nodoc:
-
2
attr_reader :object, :name, :values, :cast_type
-
-
2
def initialize(object, name, values)
-
@object = object
-
@name = name
-
@values = values
-
end
-
-
2
def read_value
-
return if values.values.compact.empty?
-
-
@cast_type = object.type_for_attribute(name)
-
klass = cast_type.klass
-
-
if klass == Time
-
read_time
-
elsif klass == Date
-
read_date
-
else
-
read_other
-
end
-
end
-
-
2
private
-
-
2
def instantiate_time_object(set_values)
-
if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
-
Time.zone.local(*set_values)
-
else
-
Time.send(object.class.default_timezone, *set_values)
-
end
-
end
-
-
2
def read_time
-
# If column is a :time (and not :date or :datetime) there is no need to validate if
-
# there are year/month/day fields
-
if cast_type.type == :time
-
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
-
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
-
values[key] ||= value
-
end
-
else
-
# else column is a timestamp, so if Date bits were not provided, error
-
validate_required_parameters!([1,2,3])
-
-
# If Date bits were provided but blank, then return nil
-
return if blank_date_parameter?
-
end
-
-
max_position = extract_max_param(6)
-
set_values = values.values_at(*(1..max_position))
-
# If Time bits are not there, then default to 0
-
(3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
-
instantiate_time_object(set_values)
-
end
-
-
2
def read_date
-
return if blank_date_parameter?
-
set_values = values.values_at(1,2,3)
-
begin
-
Date.new(*set_values)
-
rescue ArgumentError # if Date.new raises an exception on an invalid date
-
instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
-
end
-
end
-
-
2
def read_other
-
max_position = extract_max_param
-
positions = (1..max_position)
-
validate_required_parameters!(positions)
-
-
values.slice(*positions)
-
end
-
-
# Checks whether some blank date parameter exists. Note that this is different
-
# than the validate_required_parameters! method, since it just checks for blank
-
# positions instead of missing ones, and does not raise in case one blank position
-
# exists. The caller is responsible to handle the case of this returning true.
-
2
def blank_date_parameter?
-
(1..3).any? { |position| values[position].blank? }
-
end
-
-
# If some position is not provided, it errors out a missing parameter exception.
-
2
def validate_required_parameters!(positions)
-
if missing_parameter = positions.detect { |position| !values.key?(position) }
-
raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
-
end
-
end
-
-
2
def extract_max_param(upper_cap = 100)
-
[values.keys.max, upper_cap].min
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module AttributeDecorators # :nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
-
2
self.attribute_type_decorations = TypeDecorator.new
-
end
-
-
2
module ClassMethods # :nodoc:
-
2
def decorate_attribute_type(column_name, decorator_name, &block)
-
matcher = ->(name, _) { name == column_name.to_s }
-
key = "_#{column_name}_#{decorator_name}"
-
decorate_matching_attribute_types(matcher, key, &block)
-
end
-
-
2
def decorate_matching_attribute_types(matcher, decorator_name, &block)
-
24
clear_caches_calculated_from_columns
-
24
decorator_name = decorator_name.to_s
-
-
# Create new hashes so we don't modify parent classes
-
24
self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
-
end
-
-
2
private
-
-
2
def add_user_provided_columns(*)
-
12
super.map do |column|
-
88
decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
-
88
column.with_type(decorated_type)
-
end
-
end
-
end
-
-
2
class TypeDecorator # :nodoc:
-
2
delegate :clear, to: :@decorations
-
-
2
def initialize(decorations = {})
-
26
@decorations = decorations
-
end
-
-
2
def merge(*args)
-
24
TypeDecorator.new(@decorations.merge(*args))
-
end
-
-
2
def apply(name, type)
-
88
decorations = decorators_for(name, type)
-
88
decorations.inject(type) do |new_type, block|
-
28
block.call(new_type)
-
end
-
end
-
-
2
private
-
-
2
def decorators_for(name, type)
-
88
matching(name, type).map(&:last)
-
end
-
-
2
def matching(name, type)
-
88
@decorations.values.select do |(matcher, _)|
-
176
matcher.call(name, type)
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module AttributeMethods
-
# = Active Record Attribute Methods Before Type Cast
-
#
-
# <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to
-
# read the value of the attributes before typecasting and deserialization.
-
#
-
# class Task < ActiveRecord::Base
-
# end
-
#
-
# task = Task.new(id: '1', completed_on: '2012-10-21')
-
# task.id # => 1
-
# task.completed_on # => Sun, 21 Oct 2012
-
#
-
# task.attributes_before_type_cast
-
# # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
-
# task.read_attribute_before_type_cast('id') # => "1"
-
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
-
#
-
# In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
-
# it declares a method for all attributes with the <tt>*_before_type_cast</tt>
-
# suffix.
-
#
-
# task.id_before_type_cast # => "1"
-
# task.completed_on_before_type_cast # => "2012-10-21"
-
2
module BeforeTypeCast
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
attribute_method_suffix "_before_type_cast"
-
2
attribute_method_suffix "_came_from_user?"
-
end
-
-
# Returns the value of the attribute identified by +attr_name+ before
-
# typecasting and deserialization.
-
#
-
# class Task < ActiveRecord::Base
-
# end
-
#
-
# task = Task.new(id: '1', completed_on: '2012-10-21')
-
# task.read_attribute('id') # => 1
-
# task.read_attribute_before_type_cast('id') # => '1'
-
# task.read_attribute('completed_on') # => Sun, 21 Oct 2012
-
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
-
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
-
2
def read_attribute_before_type_cast(attr_name)
-
167
@attributes[attr_name.to_s].value_before_type_cast
-
end
-
-
# Returns a hash of attributes before typecasting and deserialization.
-
#
-
# class Task < ActiveRecord::Base
-
# end
-
#
-
# task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
-
# task.attributes
-
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
-
# task.attributes_before_type_cast
-
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
-
2
def attributes_before_type_cast
-
@attributes.values_before_type_cast
-
end
-
-
2
private
-
-
# Handle *_before_type_cast for method_missing.
-
2
def attribute_before_type_cast(attribute_name)
-
22
read_attribute_before_type_cast(attribute_name)
-
end
-
-
2
def attribute_came_from_user?(attribute_name)
-
28
@attributes[attribute_name].came_from_user?
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/attribute_accessors'
-
-
2
module ActiveRecord
-
2
module AttributeMethods
-
2
module Dirty # :nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
include ActiveModel::Dirty
-
-
2
included do
-
2
if self < ::ActiveRecord::Timestamp
-
raise "You cannot include Dirty after Timestamp"
-
end
-
-
2
class_attribute :partial_writes, instance_writer: false
-
2
self.partial_writes = true
-
end
-
-
# Attempts to +save+ the record and clears changed attributes if successful.
-
2
def save(*)
-
14
if status = super
-
10
changes_applied
-
end
-
14
status
-
end
-
-
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
-
2
def save!(*)
-
super.tap do
-
changes_applied
-
end
-
end
-
-
# <tt>reload</tt> the record and clears changed attributes.
-
2
def reload(*)
-
super.tap do
-
clear_changes_information
-
end
-
end
-
-
2
def initialize_dup(other) # :nodoc:
-
super
-
@original_raw_attributes = nil
-
calculate_changes_from_defaults
-
end
-
-
2
def changes_applied
-
10
super
-
10
store_original_raw_attributes
-
end
-
-
2
def clear_changes_information
-
super
-
original_raw_attributes.clear
-
end
-
-
2
def changed_attributes
-
# This should only be set by methods which will call changed_attributes
-
# multiple times when it is known that the computed value cannot change.
-
68
if defined?(@cached_changed_attributes)
-
43
@cached_changed_attributes
-
else
-
25
super.reverse_merge(attributes_changed_in_place).freeze
-
end
-
end
-
-
2
def changes
-
10
cache_changed_attributes do
-
10
super
-
end
-
end
-
-
2
def attribute_changed_in_place?(attr_name)
-
237
old_value = original_raw_attribute(attr_name)
-
237
@attributes[attr_name].changed_in_place_from?(old_value)
-
end
-
-
2
private
-
-
2
def changes_include?(attr_name)
-
109
super || attribute_changed_in_place?(attr_name)
-
end
-
-
2
def calculate_changes_from_defaults
-
@changed_attributes = nil
-
self.class.column_defaults.each do |attr, orig_value|
-
set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
-
end
-
end
-
-
# Wrap write_attribute to remember original attribute value.
-
2
def write_attribute(attr, value)
-
69
attr = attr.to_s
-
-
69
old_value = old_attribute_value(attr)
-
-
69
result = super
-
69
store_original_raw_attribute(attr)
-
69
save_changed_attribute(attr, old_value)
-
69
result
-
end
-
-
2
def raw_write_attribute(attr, value)
-
attr = attr.to_s
-
-
result = super
-
original_raw_attributes[attr] = value
-
result
-
end
-
-
2
def save_changed_attribute(attr, old_value)
-
69
clear_changed_attributes_cache
-
69
if attribute_changed_by_setter?(attr)
-
1
clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
-
else
-
68
set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
-
end
-
end
-
-
2
def old_attribute_value(attr)
-
69
if attribute_changed?(attr)
-
1
changed_attributes[attr]
-
else
-
68
clone_attribute_value(:_read_attribute, attr)
-
end
-
end
-
-
2
def _update_record(*)
-
4
partial_writes? ? super(keys_for_partial_write) : super
-
end
-
-
2
def _create_record(*)
-
6
partial_writes? ? super(keys_for_partial_write) : super
-
end
-
-
# Serialized attributes should always be written in case they've been
-
# changed in place.
-
2
def keys_for_partial_write
-
10
changed & persistable_attribute_names
-
end
-
-
2
def _field_changed?(attr, old_value)
-
69
@attributes[attr].changed_from?(old_value)
-
end
-
-
2
def attributes_changed_in_place
-
25
changed_in_place.each_with_object({}) do |attr_name, h|
-
orig = @attributes[attr_name].original_value
-
h[attr_name] = orig
-
end
-
end
-
-
2
def changed_in_place
-
25
self.class.attribute_names.select do |attr_name|
-
156
attribute_changed_in_place?(attr_name)
-
end
-
end
-
-
2
def original_raw_attribute(attr_name)
-
237
original_raw_attributes.fetch(attr_name) do
-
145
read_attribute_before_type_cast(attr_name)
-
end
-
end
-
-
2
def original_raw_attributes
-
368
@original_raw_attributes ||= {}
-
end
-
-
2
def store_original_raw_attribute(attr_name)
-
131
original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil
-
end
-
-
2
def store_original_raw_attributes
-
10
attribute_names.each do |attr|
-
62
store_original_raw_attribute(attr)
-
end
-
end
-
-
2
def cache_changed_attributes
-
10
@cached_changed_attributes = changed_attributes
-
10
yield
-
ensure
-
10
clear_changed_attributes_cache
-
end
-
-
2
def clear_changed_attributes_cache
-
79
remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
-
end
-
end
-
end
-
end
-
2
require 'set'
-
-
2
module ActiveRecord
-
2
module AttributeMethods
-
2
module PrimaryKey
-
2
extend ActiveSupport::Concern
-
-
# Returns this record's primary key value wrapped in an Array if one is
-
# available.
-
2
def to_key
-
37
sync_with_transaction_state
-
37
key = self.id
-
37
[key] if key
-
end
-
-
# Returns the primary key value.
-
2
def id
-
if pk = self.class.primary_key
-
sync_with_transaction_state
-
_read_attribute(pk)
-
end
-
end
-
-
# Sets the primary key value.
-
2
def id=(value)
-
sync_with_transaction_state
-
write_attribute(self.class.primary_key, value) if self.class.primary_key
-
end
-
-
# Queries the primary key value.
-
2
def id?
-
sync_with_transaction_state
-
query_attribute(self.class.primary_key)
-
end
-
-
# Returns the primary key value before type cast.
-
2
def id_before_type_cast
-
sync_with_transaction_state
-
read_attribute_before_type_cast(self.class.primary_key)
-
end
-
-
# Returns the primary key previous value.
-
2
def id_was
-
sync_with_transaction_state
-
attribute_was(self.class.primary_key)
-
end
-
-
2
protected
-
-
2
def attribute_method?(attr_name)
-
43
attr_name == 'id' || super
-
end
-
-
2
module ClassMethods
-
2
def define_method_attribute(attr_name)
-
56
super
-
-
56
if attr_name == primary_key && attr_name != 'id'
-
generated_attribute_methods.send(:alias_method, :id, primary_key)
-
end
-
end
-
-
2
ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set
-
-
2
def dangerous_attribute_method?(method_name)
-
640
super && !ID_ATTRIBUTE_METHODS.include?(method_name)
-
end
-
-
# Defines the primary key field -- can be overridden in subclasses.
-
# Overwriting will negate any effect of the +primary_key_prefix_type+
-
# setting, though.
-
2
def primary_key
-
569
@primary_key = reset_primary_key unless defined? @primary_key
-
569
@primary_key
-
end
-
-
# Returns a quoted version of the primary key name, used to construct
-
# SQL statements.
-
2
def quoted_primary_key
-
@quoted_primary_key ||= connection.quote_column_name(primary_key)
-
end
-
-
2
def reset_primary_key #:nodoc:
-
10
if self == base_class
-
10
self.primary_key = get_primary_key(base_class.name)
-
else
-
self.primary_key = base_class.primary_key
-
end
-
end
-
-
2
def get_primary_key(base_name) #:nodoc:
-
10
if base_name && primary_key_prefix_type == :table_name
-
base_name.foreign_key(false)
-
10
elsif base_name && primary_key_prefix_type == :table_name_with_underscore
-
base_name.foreign_key
-
else
-
10
if ActiveRecord::Base != self && table_exists?
-
10
connection.schema_cache.primary_keys(table_name)
-
else
-
'id'
-
end
-
end
-
end
-
-
# Sets the name of the primary key column.
-
#
-
# class Project < ActiveRecord::Base
-
# self.primary_key = 'sysid'
-
# end
-
#
-
# You can also define the +primary_key+ method yourself:
-
#
-
# class Project < ActiveRecord::Base
-
# def self.primary_key
-
# 'foo_' + super
-
# end
-
# end
-
#
-
# Project.primary_key # => "foo_id"
-
2
def primary_key=(value)
-
10
@primary_key = value && value.to_s
-
10
@quoted_primary_key = nil
-
10
@attributes_builder = nil
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module AttributeMethods
-
2
module Query
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
attribute_method_suffix "?"
-
end
-
-
2
def query_attribute(attr_name)
-
10
value = self[attr_name]
-
-
10
case value
-
10
when true then true
-
when false, nil then false
-
else
-
column = self.class.columns_hash[attr_name]
-
if column.nil?
-
if Numeric === value || value !~ /[^0-9]/
-
!value.to_i.zero?
-
else
-
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
-
!value.blank?
-
end
-
elsif column.number?
-
!value.zero?
-
else
-
!value.blank?
-
end
-
end
-
end
-
-
2
private
-
# Handle *? for method_missing.
-
2
def attribute?(attribute_name)
-
10
query_attribute(attribute_name)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/method_transplanting'
-
-
2
module ActiveRecord
-
2
module AttributeMethods
-
2
module Read
-
2
ReaderMethodCache = Class.new(AttributeMethodCache) {
-
2
private
-
# We want to generate the methods via module_eval rather than
-
# define_method, because define_method is slower on dispatch.
-
# Evaluating many similar methods may use more memory as the instruction
-
# sequences are duplicated and cached (in MRI). define_method may
-
# be slower on dispatch, but if you're careful about the closure
-
# created, then define_method will consume much less memory.
-
#
-
# But sometimes the database might return columns with
-
# characters that are not allowed in normal method names (like
-
# 'my_column(omg)'. So to work around this we first define with
-
# the __temp__ identifier, and then use alias method to rename
-
# it to what we want.
-
#
-
# We are also defining a constant to hold the frozen string of
-
# the attribute name. Using a constant means that we do not have
-
# to allocate an object on each call to the attribute method.
-
# Making it frozen means that it doesn't get duped when used to
-
# key the @attributes in read_attribute.
-
2
def method_body(method_name, const_name)
-
<<-EOMETHOD
-
45
def #{method_name}
-
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
-
_read_attribute(name) { |n| missing_attribute(n, caller) }
-
end
-
EOMETHOD
-
end
-
}.new
-
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
2
[:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name|
-
6
define_method method_name do |*|
-
cached_attributes_deprecation_warning(method_name)
-
true
-
end
-
end
-
-
2
protected
-
-
2
def cached_attributes_deprecation_warning(method_name)
-
ActiveSupport::Deprecation.warn "Calling `#{method_name}` is no longer necessary. All attributes are cached."
-
end
-
-
2
if Module.methods_transplantable?
-
2
def define_method_attribute(name)
-
56
method = ReaderMethodCache[name]
-
112
generated_attribute_methods.module_eval { define_method name, method }
-
end
-
else
-
def define_method_attribute(name)
-
safe_name = name.unpack('h*').first
-
temp_method = "__temp__#{safe_name}"
-
-
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
-
-
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
-
def #{temp_method}
-
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
-
_read_attribute(name) { |n| missing_attribute(n, caller) }
-
end
-
STR
-
-
generated_attribute_methods.module_eval do
-
alias_method name, temp_method
-
undef_method temp_method
-
end
-
end
-
end
-
end
-
-
2
ID = 'id'.freeze
-
-
# Returns the value of the attribute identified by <tt>attr_name</tt> after
-
# it has been typecast (for example, "2004-12-12" in a date column is cast
-
# to a date object, like Date.new(2004, 12, 12)).
-
2
def read_attribute(attr_name, &block)
-
25
name = attr_name.to_s
-
25
name = self.class.primary_key if name == ID
-
25
_read_attribute(name, &block)
-
end
-
-
# This method exists to avoid the expensive primary_key check internally, without
-
# breaking compatibility with the read_attribute API
-
2
def _read_attribute(attr_name) # :nodoc:
-
730
@attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
-
end
-
-
2
private
-
-
2
def attribute(attribute_name)
-
_read_attribute(attribute_name)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
2
module AttributeMethods
-
2
module Serialization
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
# If you have an attribute that needs to be saved to the database as an
-
# object, and retrieved as the same object, then specify the name of that
-
# attribute using this method and it will be handled automatically. The
-
# serialization is done through YAML. If +class_name+ is specified, the
-
# serialized object must be of that class on assignment and retrieval.
-
# Otherwise <tt>SerializationTypeMismatch</tt> will be raised.
-
#
-
# ==== Parameters
-
#
-
# * +attr_name+ - The field name that should be serialized.
-
# * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump`
-
# or a class name that the object type should be equal to.
-
#
-
# ==== Example
-
#
-
# # Serialize a preferences attribute.
-
# class User < ActiveRecord::Base
-
# serialize :preferences
-
# end
-
#
-
# # Serialize preferences using JSON as coder.
-
# class User < ActiveRecord::Base
-
# serialize :preferences, JSON
-
# end
-
#
-
# # Serialize preferences as Hash using YAML coder.
-
# class User < ActiveRecord::Base
-
# serialize :preferences, Hash
-
# end
-
2
def serialize(attr_name, class_name_or_coder = Object)
-
# When ::JSON is used, force it to go through the Active Support JSON encoder
-
# to ensure special objects (e.g. Active Record models) are dumped correctly
-
# using the #as_json hook.
-
coder = if class_name_or_coder == ::JSON
-
Coders::JSON
-
elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
-
class_name_or_coder
-
else
-
Coders::YAMLColumn.new(class_name_or_coder)
-
end
-
-
decorate_attribute_type(attr_name, :serialize) do |type|
-
Type::Serialized.new(type, coder)
-
end
-
end
-
-
2
def serialized_attributes
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
`serialized_attributes` is deprecated without replacement, and will
-
be removed in Rails 5.0.
-
MSG
-
-
@serialized_attributes ||= Hash[
-
columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
-
[c.name, c.cast_type.coder]
-
}
-
]
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module AttributeMethods
-
2
module TimeZoneConversion
-
2
class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc:
-
2
include Type::Decorator
-
-
2
def type_cast_from_database(value)
-
20
convert_time_to_time_zone(super)
-
end
-
-
2
def type_cast_from_user(value)
-
14
if value.is_a?(Array)
-
value.map { |v| type_cast_from_user(v) }
-
14
elsif value.respond_to?(:in_time_zone)
-
14
begin
-
14
value.in_time_zone || super
-
rescue ArgumentError
-
nil
-
end
-
end
-
end
-
-
2
def convert_time_to_time_zone(value)
-
20
if value.is_a?(Array)
-
value.map { |v| convert_time_to_time_zone(v) }
-
20
elsif value.acts_like?(:time)
-
8
value.in_time_zone
-
else
-
12
value
-
end
-
end
-
end
-
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
mattr_accessor :time_zone_aware_attributes, instance_writer: false
-
2
self.time_zone_aware_attributes = false
-
-
2
class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
-
2
self.skip_time_zone_conversion_for_attributes = []
-
end
-
-
2
module ClassMethods
-
2
private
-
-
2
def inherited(subclass)
-
# We need to apply this decorator here, rather than on module inclusion. The closure
-
# created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
-
# sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
-
# `skip_time_zone_conversion_for_attributes` would not be picked up.
-
12
subclass.class_eval do
-
100
matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
-
12
decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
-
28
TimeZoneConverter.new(type)
-
end
-
end
-
12
super
-
end
-
-
2
def create_time_zone_conversion_attribute?(name, cast_type)
-
time_zone_aware_attributes &&
-
88
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
-
88
(:datetime == cast_type.type)
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/method_transplanting'
-
-
2
module ActiveRecord
-
2
module AttributeMethods
-
2
module Write
-
2
WriterMethodCache = Class.new(AttributeMethodCache) {
-
2
private
-
-
2
def method_body(method_name, const_name)
-
<<-EOMETHOD
-
45
def #{method_name}(value)
-
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
-
write_attribute(name, value)
-
end
-
EOMETHOD
-
end
-
}.new
-
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
attribute_method_suffix "="
-
end
-
-
2
module ClassMethods
-
2
protected
-
-
2
if Module.methods_transplantable?
-
2
def define_method_attribute=(name)
-
56
method = WriterMethodCache[name]
-
56
generated_attribute_methods.module_eval {
-
56
define_method "#{name}=", method
-
}
-
end
-
else
-
def define_method_attribute=(name)
-
safe_name = name.unpack('h*').first
-
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
-
-
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
-
def __temp__#{safe_name}=(value)
-
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
-
write_attribute(name, value)
-
end
-
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
-
undef_method :__temp__#{safe_name}=
-
STR
-
end
-
end
-
end
-
-
# Updates the attribute identified by <tt>attr_name</tt> with the
-
# specified +value+. Empty strings for fixnum and float columns are
-
# turned into +nil+.
-
2
def write_attribute(attr_name, value)
-
69
write_attribute_with_type_cast(attr_name, value, true)
-
end
-
-
2
def raw_write_attribute(attr_name, value)
-
write_attribute_with_type_cast(attr_name, value, false)
-
end
-
-
2
private
-
# Handle *= for method_missing.
-
2
def attribute=(attribute_name, value)
-
write_attribute(attribute_name, value)
-
end
-
-
2
def write_attribute_with_type_cast(attr_name, value, should_type_cast)
-
69
attr_name = attr_name.to_s
-
69
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
-
-
69
if should_type_cast
-
69
@attributes.write_from_user(attr_name, value)
-
else
-
@attributes.write_cast_value(attr_name, value)
-
end
-
-
69
value
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Attributes # :nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
Type = ActiveRecord::Type
-
-
2
included do
-
2
class_attribute :user_provided_columns, instance_accessor: false # :internal:
-
2
class_attribute :user_provided_defaults, instance_accessor: false # :internal:
-
2
self.user_provided_columns = {}
-
2
self.user_provided_defaults = {}
-
-
2
delegate :persistable_attribute_names, to: :class
-
end
-
-
2
module ClassMethods # :nodoc:
-
# Defines or overrides a attribute on this model. This allows customization of
-
# Active Record's type casting behavior, as well as adding support for user defined
-
# types.
-
#
-
# +name+ The name of the methods to define attribute methods for, and the column which
-
# this will persist to.
-
#
-
# +cast_type+ A type object that contains information about how to type cast the value.
-
# See the examples section for more information.
-
#
-
# ==== Options
-
# The options hash accepts the following options:
-
#
-
# +default+ is the default value that the column should use on a new record.
-
#
-
# ==== Examples
-
#
-
# The type detected by Active Record can be overridden.
-
#
-
# # db/schema.rb
-
# create_table :store_listings, force: true do |t|
-
# t.decimal :price_in_cents
-
# end
-
#
-
# # app/models/store_listing.rb
-
# class StoreListing < ActiveRecord::Base
-
# end
-
#
-
# store_listing = StoreListing.new(price_in_cents: '10.1')
-
#
-
# # before
-
# store_listing.price_in_cents # => BigDecimal.new(10.1)
-
#
-
# class StoreListing < ActiveRecord::Base
-
# attribute :price_in_cents, Type::Integer.new
-
# end
-
#
-
# # after
-
# store_listing.price_in_cents # => 10
-
#
-
# Users may also define their own custom types, as long as they respond to the methods
-
# defined on the value type. The `type_cast` method on your type object will be called
-
# with values both from the database, and from your controllers. See
-
# `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your
-
# type objects inherit from an existing type, or the base value type.
-
#
-
# class MoneyType < ActiveRecord::Type::Integer
-
# def type_cast(value)
-
# if value.include?('$')
-
# price_in_dollars = value.gsub(/\$/, '').to_f
-
# price_in_dollars * 100
-
# else
-
# value.to_i
-
# end
-
# end
-
# end
-
#
-
# class StoreListing < ActiveRecord::Base
-
# attribute :price_in_cents, MoneyType.new
-
# end
-
#
-
# store_listing = StoreListing.new(price_in_cents: '$10.00')
-
# store_listing.price_in_cents # => 1000
-
2
def attribute(name, cast_type, options = {})
-
name = name.to_s
-
clear_caches_calculated_from_columns
-
# Assign a new hash to ensure that subclasses do not share a hash
-
self.user_provided_columns = user_provided_columns.merge(name => cast_type)
-
-
if options.key?(:default)
-
self.user_provided_defaults = user_provided_defaults.merge(name => options[:default])
-
end
-
end
-
-
# Returns an array of column objects for the table associated with this class.
-
2
def columns
-
29
@columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
-
end
-
-
# Returns a hash of column objects for the table associated with this class.
-
2
def columns_hash
-
567
@columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
-
end
-
-
2
def persistable_attribute_names # :nodoc:
-
10
@persistable_attribute_names ||= connection.schema_cache.columns_hash(table_name).keys
-
end
-
-
2
def reset_column_information # :nodoc:
-
super
-
clear_caches_calculated_from_columns
-
end
-
-
2
private
-
-
2
def add_user_provided_columns(schema_columns)
-
12
existing_columns = schema_columns.map do |column|
-
88
new_type = user_provided_columns[column.name]
-
88
if new_type
-
column.with_type(new_type)
-
else
-
88
column
-
end
-
end
-
-
12
existing_column_names = existing_columns.map(&:name)
-
12
new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)|
-
connection.new_column(name, nil, type)
-
end
-
-
12
existing_columns + new_columns
-
end
-
-
2
def clear_caches_calculated_from_columns
-
36
@attributes_builder = nil
-
36
@column_names = nil
-
36
@column_types = nil
-
36
@columns = nil
-
36
@columns_hash = nil
-
36
@content_columns = nil
-
36
@default_attributes = nil
-
36
@persistable_attribute_names = nil
-
end
-
-
2
def raw_default_values
-
5
super.merge(user_provided_defaults)
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record Autosave Association
-
#
-
# +AutosaveAssociation+ is a module that takes care of automatically saving
-
# associated records when their parent is saved. In addition to saving, it
-
# also destroys any associated records that were marked for destruction.
-
# (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
-
#
-
# Saving of the parent, its associations, and the destruction of marked
-
# associations, all happen inside a transaction. This should never leave the
-
# database in an inconsistent state.
-
#
-
# If validations for any of the associations fail, their error messages will
-
# be applied to the parent.
-
#
-
# Note that it also means that associations marked for destruction won't
-
# be destroyed directly. They will however still be marked for destruction.
-
#
-
# Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
-
# When the <tt>:autosave</tt> option is not present then new association records are
-
# saved but the updated association records are not saved.
-
#
-
# == Validation
-
#
-
# Children records are validated unless <tt>:validate</tt> is +false+.
-
#
-
# == Callbacks
-
#
-
# Association with autosave option defines several callbacks on your
-
# model (before_save, after_create, after_update). Please note that
-
# callbacks are executed in the order they were defined in
-
# model. You should avoid modifying the association content, before
-
# autosave callbacks are executed. Placing your callbacks after
-
# associations is usually a good practice.
-
#
-
# === One-to-one Example
-
#
-
# class Post < ActiveRecord::Base
-
# has_one :author, autosave: true
-
# end
-
#
-
# Saving changes to the parent and its associated model can now be performed
-
# automatically _and_ atomically:
-
#
-
# post = Post.find(1)
-
# post.title # => "The current global position of migrating ducks"
-
# post.author.name # => "alloy"
-
#
-
# post.title = "On the migration of ducks"
-
# post.author.name = "Eloy Duran"
-
#
-
# post.save
-
# post.reload
-
# post.title # => "On the migration of ducks"
-
# post.author.name # => "Eloy Duran"
-
#
-
# Destroying an associated model, as part of the parent's save action, is as
-
# simple as marking it for destruction:
-
#
-
# post.author.mark_for_destruction
-
# post.author.marked_for_destruction? # => true
-
#
-
# Note that the model is _not_ yet removed from the database:
-
#
-
# id = post.author.id
-
# Author.find_by(id: id).nil? # => false
-
#
-
# post.save
-
# post.reload.author # => nil
-
#
-
# Now it _is_ removed from the database:
-
#
-
# Author.find_by(id: id).nil? # => true
-
#
-
# === One-to-many Example
-
#
-
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :comments # :autosave option is not declared
-
# end
-
#
-
# post = Post.new(title: 'ruby rocks')
-
# post.comments.build(body: 'hello world')
-
# post.save # => saves both post and comment
-
#
-
# post = Post.create(title: 'ruby rocks')
-
# post.comments.build(body: 'hello world')
-
# post.save # => saves both post and comment
-
#
-
# post = Post.create(title: 'ruby rocks')
-
# post.comments.create(body: 'hello world')
-
# post.save # => saves both post and comment
-
#
-
# When <tt>:autosave</tt> is true all children are saved, no matter whether they
-
# are new records or not:
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :comments, autosave: true
-
# end
-
#
-
# post = Post.create(title: 'ruby rocks')
-
# post.comments.create(body: 'hello world')
-
# post.comments[0].body = 'hi everyone'
-
# post.comments.build(body: "good morning.")
-
# post.title += "!"
-
# post.save # => saves both post and comments.
-
#
-
# Destroying one of the associated models as part of the parent's save action
-
# is as simple as marking it for destruction:
-
#
-
# post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
-
# post.comments[1].mark_for_destruction
-
# post.comments[1].marked_for_destruction? # => true
-
# post.comments.length # => 2
-
#
-
# Note that the model is _not_ yet removed from the database:
-
#
-
# id = post.comments.last.id
-
# Comment.find_by(id: id).nil? # => false
-
#
-
# post.save
-
# post.reload.comments.length # => 1
-
#
-
# Now it _is_ removed from the database:
-
#
-
# Comment.find_by(id: id).nil? # => true
-
-
2
module AutosaveAssociation
-
2
extend ActiveSupport::Concern
-
-
2
module AssociationBuilderExtension #:nodoc:
-
2
def self.build(model, reflection)
-
24
model.send(:add_autosave_association_callbacks, reflection)
-
end
-
-
2
def self.valid_options
-
24
[ :autosave ]
-
end
-
end
-
-
2
included do
-
2
Associations::Builder::Association.extensions << AssociationBuilderExtension
-
end
-
-
2
module ClassMethods
-
2
private
-
-
2
def define_non_cyclic_method(name, &block)
-
42
return if method_defined?(name)
-
42
define_method(name) do |*args|
-
56
result = true; @_already_called ||= {}
-
# Loop prevention for validation of associations
-
56
unless @_already_called[name]
-
56
begin
-
56
@_already_called[name]=true
-
56
result = instance_eval(&block)
-
ensure
-
56
@_already_called[name]=false
-
end
-
end
-
-
56
result
-
end
-
end
-
-
# Adds validation and save callbacks for the association as specified by
-
# the +reflection+.
-
#
-
# For performance reasons, we don't check whether to validate at runtime.
-
# However the validation and callback methods are lazy and those methods
-
# get created when they are invoked for the very first time. However,
-
# this can change, for instance, when using nested attributes, which is
-
# called _after_ the association has been defined. Since we don't want
-
# the callbacks to get defined multiple times, there are guards that
-
# check if the save or validation methods have already been defined
-
# before actually defining them.
-
2
def add_autosave_association_callbacks(reflection)
-
24
save_method = :"autosave_associated_records_for_#{reflection.name}"
-
-
24
if reflection.collection?
-
16
before_save :before_save_collection_association
-
-
24
define_non_cyclic_method(save_method) { save_collection_association(reflection) }
-
# Doesn't use after_save as that would save associations added in after_create/after_update twice
-
16
after_create save_method
-
16
after_update save_method
-
elsif reflection.has_one?
-
define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
-
# Configures two callbacks instead of a single after_save so that
-
# the model may rely on their execution order relative to its
-
# own callbacks.
-
#
-
# For example, given that after_creates run before after_saves, if
-
# we configured instead an after_save there would be no way to fire
-
# a custom after_create callback after the child association gets
-
# created.
-
after_create save_method
-
after_update save_method
-
else
-
20
define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
-
8
before_save save_method
-
end
-
-
24
define_autosave_validation_callbacks(reflection)
-
end
-
-
2
def define_autosave_validation_callbacks(reflection)
-
32
validation_method = :"validate_associated_records_for_#{reflection.name}"
-
32
if reflection.validate? && !method_defined?(validation_method)
-
18
if reflection.collection?
-
16
method = :validate_collection_association
-
else
-
2
method = :validate_single_association
-
end
-
-
54
define_non_cyclic_method(validation_method) { send(method, reflection) }
-
18
validate validation_method
-
end
-
end
-
end
-
-
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
-
2
def reload(options = nil)
-
@marked_for_destruction = false
-
@destroyed_by_association = nil
-
super
-
end
-
-
# Marks this record to be destroyed as part of the parents save transaction.
-
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
-
# when <tt>parent.save</tt> is called.
-
#
-
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
-
2
def mark_for_destruction
-
@marked_for_destruction = true
-
end
-
-
# Returns whether or not this record will be destroyed as part of the parents save transaction.
-
#
-
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
-
2
def marked_for_destruction?
-
@marked_for_destruction
-
end
-
-
# Records the association that is being destroyed and destroying this
-
# record in the process.
-
2
def destroyed_by_association=(reflection)
-
@destroyed_by_association = reflection
-
end
-
-
# Returns the association for the parent being destroyed.
-
#
-
# Used to avoid updating the counter cache unnecessarily.
-
2
def destroyed_by_association
-
@destroyed_by_association
-
end
-
-
# Returns whether or not this record has been changed in any way (including whether
-
# any of its nested autosave associations are likewise changed)
-
2
def changed_for_autosave?
-
new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
-
end
-
-
2
private
-
-
# Returns the record for an association collection that should be validated
-
# or saved. If +autosave+ is +false+ only new records will be returned,
-
# unless the parent is/was a new record itself.
-
2
def associated_records_to_validate_or_save(association, new_record, autosave)
-
if new_record
-
association && association.target
-
elsif autosave
-
association.target.find_all { |record| record.changed_for_autosave? }
-
else
-
association.target.find_all { |record| record.new_record? }
-
end
-
end
-
-
# go through nested autosave associations that are loaded in memory (without loading
-
# any new ones), and return true if is changed for autosave
-
2
def nested_records_changed_for_autosave?
-
@_nested_records_changed_for_autosave_already_called ||= false
-
return false if @_nested_records_changed_for_autosave_already_called
-
begin
-
@_nested_records_changed_for_autosave_already_called = true
-
self.class._reflections.values.any? do |reflection|
-
if reflection.options[:autosave]
-
association = association_instance_get(reflection.name)
-
association && Array.wrap(association.target).any?(&:changed_for_autosave?)
-
end
-
end
-
ensure
-
@_nested_records_changed_for_autosave_already_called = false
-
end
-
end
-
-
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
-
# turned on for the association.
-
2
def validate_single_association(reflection)
-
6
association = association_instance_get(reflection.name)
-
6
record = association && association.reader
-
6
association_valid?(reflection, record) if record
-
end
-
-
# Validate the associated records if <tt>:validate</tt> or
-
# <tt>:autosave</tt> is turned on for the association specified by
-
# +reflection+.
-
2
def validate_collection_association(reflection)
-
30
if association = association_instance_get(reflection.name)
-
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
-
records.each { |record| association_valid?(reflection, record) }
-
end
-
end
-
end
-
-
# Returns whether or not the association is valid and applies any errors to
-
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
-
# enabled records if they're marked_for_destruction? or destroyed.
-
2
def association_valid?(reflection, record)
-
return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
-
-
validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
-
unless valid = record.valid?(validation_context)
-
if reflection.options[:autosave]
-
record.errors.each do |attribute, message|
-
attribute = "#{reflection.name}.#{attribute}"
-
errors[attribute] << message
-
errors[attribute].uniq!
-
end
-
else
-
errors.add(reflection.name)
-
end
-
end
-
valid
-
end
-
-
# Is used as a before_save callback to check while saving a collection
-
# association whether or not the parent was a new record before saving.
-
2
def before_save_collection_association
-
4
@new_record_before_save = new_record?
-
4
true
-
end
-
-
# Saves any new associated records, or all loaded autosave associations if
-
# <tt>:autosave</tt> is enabled on the association.
-
#
-
# In addition, it destroys all children that were marked for destruction
-
# with mark_for_destruction.
-
#
-
# This all happens inside a transaction, _if_ the Transactions module is included into
-
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
-
2
def save_collection_association(reflection)
-
8
if association = association_instance_get(reflection.name)
-
autosave = reflection.options[:autosave]
-
-
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
-
if autosave
-
records_to_destroy = records.select(&:marked_for_destruction?)
-
records_to_destroy.each { |record| association.destroy(record) }
-
records -= records_to_destroy
-
end
-
-
records.each do |record|
-
next if record.destroyed?
-
-
saved = true
-
-
if autosave != false && (@new_record_before_save || record.new_record?)
-
if autosave
-
saved = association.insert_record(record, false)
-
else
-
association.insert_record(record) unless reflection.nested?
-
end
-
elsif autosave
-
saved = record.save(:validate => false)
-
end
-
-
raise ActiveRecord::Rollback unless saved
-
end
-
end
-
-
# reconstruct the scope now that we know the owner's id
-
association.reset_scope if association.respond_to?(:reset_scope)
-
end
-
end
-
-
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled
-
# on the association.
-
#
-
# In addition, it will destroy the association if it was marked for
-
# destruction with mark_for_destruction.
-
#
-
# This all happens inside a transaction, _if_ the Transactions module is included into
-
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
-
2
def save_has_one_association(reflection)
-
association = association_instance_get(reflection.name)
-
record = association && association.load_target
-
-
if record && !record.destroyed?
-
autosave = reflection.options[:autosave]
-
-
if autosave && record.marked_for_destruction?
-
record.destroy
-
elsif autosave != false
-
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
-
-
if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
-
unless reflection.through_reflection
-
record[reflection.foreign_key] = key
-
end
-
-
saved = record.save(:validate => !autosave)
-
raise ActiveRecord::Rollback if !saved && autosave
-
saved
-
end
-
end
-
end
-
end
-
-
# If the record is new or it has changed, returns true.
-
2
def record_changed?(reflection, record, key)
-
record.new_record? ||
-
(record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) ||
-
record.attribute_changed?(reflection.foreign_key)
-
end
-
-
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
-
#
-
# In addition, it will destroy the association if it was marked for destruction.
-
2
def save_belongs_to_association(reflection)
-
12
association = association_instance_get(reflection.name)
-
12
record = association && association.load_target
-
12
if record && !record.destroyed?
-
autosave = reflection.options[:autosave]
-
-
if autosave && record.marked_for_destruction?
-
self[reflection.foreign_key] = nil
-
record.destroy
-
elsif autosave != false
-
saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
-
-
if association.updated?
-
association_id = record.send(reflection.options[:primary_key] || :id)
-
self[reflection.foreign_key] = association_id
-
association.loaded!
-
end
-
-
saved if autosave
-
end
-
end
-
end
-
end
-
end
-
2
require 'yaml'
-
2
require 'set'
-
2
require 'active_support/benchmarkable'
-
2
require 'active_support/dependencies'
-
2
require 'active_support/descendants_tracker'
-
2
require 'active_support/time'
-
2
require 'active_support/core_ext/module/attribute_accessors'
-
2
require 'active_support/core_ext/class/delegating_attributes'
-
2
require 'active_support/core_ext/array/extract_options'
-
2
require 'active_support/core_ext/hash/deep_merge'
-
2
require 'active_support/core_ext/hash/slice'
-
2
require 'active_support/core_ext/hash/transform_values'
-
2
require 'active_support/core_ext/string/behavior'
-
2
require 'active_support/core_ext/kernel/singleton_class'
-
2
require 'active_support/core_ext/module/introspection'
-
2
require 'active_support/core_ext/object/duplicable'
-
2
require 'active_support/core_ext/class/subclasses'
-
2
require 'arel'
-
2
require 'active_record/attribute_decorators'
-
2
require 'active_record/errors'
-
2
require 'active_record/log_subscriber'
-
2
require 'active_record/explain_subscriber'
-
2
require 'active_record/relation/delegation'
-
2
require 'active_record/attributes'
-
-
2
module ActiveRecord #:nodoc:
-
# = Active Record
-
#
-
# Active Record objects don't specify their attributes directly, but rather infer them from
-
# the table definition with which they're linked. Adding, removing, and changing attributes
-
# and their type is done directly in the database. Any change is instantly reflected in the
-
# Active Record objects. The mapping that binds a given Active Record class to a certain
-
# database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
-
#
-
# See the mapping rules in table_name and the full example in link:files/activerecord/README_rdoc.html for more insight.
-
#
-
# == Creation
-
#
-
# Active Records accept constructor parameters either in a hash or as a block. The hash
-
# method is especially useful when you're receiving the data from somewhere else, like an
-
# HTTP request. It works like this:
-
#
-
# user = User.new(name: "David", occupation: "Code Artist")
-
# user.name # => "David"
-
#
-
# You can also use block initialization:
-
#
-
# user = User.new do |u|
-
# u.name = "David"
-
# u.occupation = "Code Artist"
-
# end
-
#
-
# And of course you can just create a bare object and specify the attributes after the fact:
-
#
-
# user = User.new
-
# user.name = "David"
-
# user.occupation = "Code Artist"
-
#
-
# == Conditions
-
#
-
# Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement.
-
# The array form is to be used when the condition input is tainted and requires sanitization. The string form can
-
# be used for statements that don't involve tainted data. The hash form works much like the array form, except
-
# only equality and range is possible. Examples:
-
#
-
# class User < ActiveRecord::Base
-
# def self.authenticate_unsafely(user_name, password)
-
# where("user_name = '#{user_name}' AND password = '#{password}'").first
-
# end
-
#
-
# def self.authenticate_safely(user_name, password)
-
# where("user_name = ? AND password = ?", user_name, password).first
-
# end
-
#
-
# def self.authenticate_safely_simply(user_name, password)
-
# where(user_name: user_name, password: password).first
-
# end
-
# end
-
#
-
# The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query
-
# and is thus susceptible to SQL-injection attacks if the <tt>user_name</tt> and +password+
-
# parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and
-
# <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+
-
# before inserting them in the query, which will ensure that an attacker can't escape the
-
# query and fake the login (or worse).
-
#
-
# When using multiple parameters in the conditions, it can easily become hard to read exactly
-
# what the fourth or fifth question mark is supposed to represent. In those cases, you can
-
# resort to named bind variables instead. That's done by replacing the question marks with
-
# symbols and supplying a hash with values for the matching symbol keys:
-
#
-
# Company.where(
-
# "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
-
# { id: 3, name: "37signals", division: "First", accounting_date: '2005-01-01' }
-
# ).first
-
#
-
# Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
-
# operator. For instance:
-
#
-
# Student.where(first_name: "Harvey", status: 1)
-
# Student.where(params[:student])
-
#
-
# A range may be used in the hash to use the SQL BETWEEN operator:
-
#
-
# Student.where(grade: 9..12)
-
#
-
# An array may be used in the hash to use the SQL IN operator:
-
#
-
# Student.where(grade: [9,11,12])
-
#
-
# When joining tables, nested hashes or keys written in the form 'table_name.column_name'
-
# can be used to qualify the table name of a particular condition. For instance:
-
#
-
# Student.joins(:schools).where(schools: { category: 'public' })
-
# Student.joins(:schools).where('schools.category' => 'public' )
-
#
-
# == Overwriting default accessors
-
#
-
# All column values are automatically available through basic accessors on the Active Record
-
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
-
# the default accessors (using the same name as the attribute) and calling
-
# +super+ to actually change things.
-
#
-
# class Song < ActiveRecord::Base
-
# # Uses an integer of seconds to hold the length of the song
-
#
-
# def length=(minutes)
-
# super(minutes.to_i * 60)
-
# end
-
#
-
# def length
-
# super / 60
-
# end
-
# end
-
#
-
# You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt>
-
# or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>.
-
#
-
# == Attribute query methods
-
#
-
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
-
# Query methods allow you to test whether an attribute value is present.
-
# For numeric values, present is defined as non-zero.
-
#
-
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
-
# to determine whether the user has a name:
-
#
-
# user = User.new(name: "David")
-
# user.name? # => true
-
#
-
# anonymous = User.new(name: "")
-
# anonymous.name? # => false
-
#
-
# == Accessing attributes before they have been typecasted
-
#
-
# Sometimes you want to be able to read the raw attribute data without having the column-determined
-
# typecast run its course first. That can be done by using the <tt><attribute>_before_type_cast</tt>
-
# accessors that all attributes have. For example, if your Account model has a <tt>balance</tt> attribute,
-
# you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>.
-
#
-
# This is especially useful in validation situations where the user might supply a string for an
-
# integer field and you want to display the original string back in an error message. Accessing the
-
# attribute normally would typecast the string to 0, which isn't what you want.
-
#
-
# == Dynamic attribute-based finders
-
#
-
# Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
-
# by simple queries without turning to SQL. They work by appending the name of an attribute
-
# to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
-
# Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
-
# <tt>Person.find_by_user_name(user_name)</tt>.
-
#
-
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
-
# <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
-
# like <tt>Person.find_by_last_name!</tt>.
-
#
-
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
-
#
-
# Person.find_by(user_name: user_name, password: password)
-
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
-
#
-
# It's even possible to call these dynamic finder methods on relations and named scopes.
-
#
-
# Payment.order("created_on").find_by_amount(50)
-
#
-
# == Saving arrays, hashes, and other non-mappable objects in text columns
-
#
-
# Active Record can serialize any object in text columns using YAML. To do so, you must
-
# specify this with a call to the class method +serialize+.
-
# This makes it possible to store arrays, hashes, and other non-mappable objects without doing
-
# any additional work.
-
#
-
# class User < ActiveRecord::Base
-
# serialize :preferences
-
# end
-
#
-
# user = User.create(preferences: { "background" => "black", "display" => large })
-
# User.find(user.id).preferences # => { "background" => "black", "display" => large }
-
#
-
# You can also specify a class option as the second parameter that'll raise an exception
-
# if a serialized object is retrieved as a descendant of a class not in the hierarchy.
-
#
-
# class User < ActiveRecord::Base
-
# serialize :preferences, Hash
-
# end
-
#
-
# user = User.create(preferences: %w( one two three ))
-
# User.find(user.id).preferences # raises SerializationTypeMismatch
-
#
-
# When you specify a class option, the default value for that attribute will be a new
-
# instance of that class.
-
#
-
# class User < ActiveRecord::Base
-
# serialize :preferences, OpenStruct
-
# end
-
#
-
# user = User.new
-
# user.preferences.theme_color = "red"
-
#
-
#
-
# == Single table inheritance
-
#
-
# Active Record allows inheritance by storing the name of the class in a
-
# column that is named "type" by default. See ActiveRecord::Inheritance for
-
# more details.
-
#
-
# == Connection to multiple databases in different models
-
#
-
# Connections are usually created through ActiveRecord::Base.establish_connection and retrieved
-
# by ActiveRecord::Base.connection. All classes inheriting from ActiveRecord::Base will use this
-
# connection. But you can also set a class-specific connection. For example, if Course is an
-
# ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
-
# and Course and all of its subclasses will use this connection instead.
-
#
-
# This feature is implemented by keeping a connection pool in ActiveRecord::Base that is
-
# a Hash indexed by the class. If a connection is requested, the retrieve_connection method
-
# will go up the class-hierarchy until a connection is found in the connection pool.
-
#
-
# == Exceptions
-
#
-
# * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record.
-
# * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an
-
# <tt>:adapter</tt> key.
-
# * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a
-
# non-existent adapter
-
# (or a bad spelling of an existing one).
-
# * AssociationTypeMismatch - The object assigned to the association wasn't of the type
-
# specified in the association definition.
-
# * AttributeAssignmentError - An error occurred while doing a mass assignment through the
-
# <tt>attributes=</tt> method.
-
# You can inspect the +attribute+ property of the exception object to determine which attribute
-
# triggered the error.
-
# * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt>
-
# before querying.
-
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
-
# <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
-
# AttributeAssignmentError
-
# objects that should be inspected to determine which attributes triggered the errors.
-
# * RecordInvalid - raised by save! and create! when the record is invalid.
-
# * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
-
# or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
-
# nothing was found, please check its documentation for further details.
-
# * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
-
# * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
-
#
-
# *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
-
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
-
# instances in the current object space.
-
2
class Base
-
2
extend ActiveModel::Naming
-
-
2
extend ActiveSupport::Benchmarkable
-
2
extend ActiveSupport::DescendantsTracker
-
-
2
extend ConnectionHandling
-
2
extend QueryCache::ClassMethods
-
2
extend Querying
-
2
extend Translation
-
2
extend DynamicMatchers
-
2
extend Explain
-
2
extend Enum
-
2
extend Delegation::DelegateCache
-
-
2
include Core
-
2
include Persistence
-
2
include ReadonlyAttributes
-
2
include ModelSchema
-
2
include Inheritance
-
2
include Scoping
-
2
include Sanitization
-
2
include AttributeAssignment
-
2
include ActiveModel::Conversion
-
2
include Integration
-
2
include Validations
-
2
include CounterCache
-
2
include Attributes
-
2
include AttributeDecorators
-
2
include Locking::Optimistic
-
2
include Locking::Pessimistic
-
2
include AttributeMethods
-
2
include Callbacks
-
2
include Timestamp
-
2
include Associations
-
2
include ActiveModel::SecurePassword
-
2
include AutosaveAssociation
-
2
include NestedAttributes
-
2
include Aggregations
-
2
include Transactions
-
2
include NoTouching
-
2
include Reflection
-
2
include Serialization
-
2
include Store
-
end
-
-
2
ActiveSupport.run_load_hooks(:active_record, Base)
-
end
-
2
module ActiveRecord
-
# = Active Record Callbacks
-
#
-
# Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic
-
# before or after an alteration of the object state. This can be used to make sure that associated and
-
# dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
-
# before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
-
# the <tt>Base#save</tt> call for a new record:
-
#
-
# * (-) <tt>save</tt>
-
# * (-) <tt>valid</tt>
-
# * (1) <tt>before_validation</tt>
-
# * (-) <tt>validate</tt>
-
# * (2) <tt>after_validation</tt>
-
# * (3) <tt>before_save</tt>
-
# * (4) <tt>before_create</tt>
-
# * (-) <tt>create</tt>
-
# * (5) <tt>after_create</tt>
-
# * (6) <tt>after_save</tt>
-
# * (7) <tt>after_commit</tt>
-
#
-
# Also, an <tt>after_rollback</tt> callback can be configured to be triggered whenever a rollback is issued.
-
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
-
# <tt>after_rollback</tt>.
-
#
-
# Additionally, an <tt>after_touch</tt> callback is triggered whenever an
-
# object is touched.
-
#
-
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
-
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
-
# are instantiated as well.
-
#
-
# There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
-
# Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
-
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
-
#
-
# Examples:
-
# class CreditCard < ActiveRecord::Base
-
# # Strip everything but digits, so the user can specify "555 234 34" or
-
# # "5552-3434" and both will mean "55523434"
-
# before_validation(on: :create) do
-
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
-
# end
-
# end
-
#
-
# class Subscription < ActiveRecord::Base
-
# before_create :record_signup
-
#
-
# private
-
# def record_signup
-
# self.signed_up_on = Date.today
-
# end
-
# end
-
#
-
# class Firm < ActiveRecord::Base
-
# # Destroys the associated clients and people when the firm is destroyed
-
# before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
-
# before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
-
# end
-
#
-
# == Inheritable callback queues
-
#
-
# Besides the overwritable callback methods, it's also possible to register callbacks through the
-
# use of the callback macros. Their main advantage is that the macros add behavior into a callback
-
# queue that is kept intact down through an inheritance hierarchy.
-
#
-
# class Topic < ActiveRecord::Base
-
# before_destroy :destroy_author
-
# end
-
#
-
# class Reply < Topic
-
# before_destroy :destroy_readers
-
# end
-
#
-
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
-
# run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
-
# where the +before_destroy+ method is overridden:
-
#
-
# class Topic < ActiveRecord::Base
-
# def before_destroy() destroy_author end
-
# end
-
#
-
# class Reply < Topic
-
# def before_destroy() destroy_readers end
-
# end
-
#
-
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
-
# So, use the callback macros when you want to ensure that a certain callback is called for the entire
-
# hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
-
# to decide whether they want to call +super+ and trigger the inherited callbacks.
-
#
-
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
-
# callbacks before specifying the associations. Otherwise, you might trigger the loading of a
-
# child before the parent has registered the callbacks and they won't be inherited.
-
#
-
# == Types of callbacks
-
#
-
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
-
# inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
-
# are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
-
# creating mix-ins), and inline eval methods are deprecated.
-
#
-
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
-
#
-
# class Topic < ActiveRecord::Base
-
# before_destroy :delete_parents
-
#
-
# private
-
# def delete_parents
-
# self.class.delete_all "parent_id = #{id}"
-
# end
-
# end
-
#
-
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
-
#
-
# class BankAccount < ActiveRecord::Base
-
# before_save EncryptionWrapper.new
-
# after_save EncryptionWrapper.new
-
# after_initialize EncryptionWrapper.new
-
# end
-
#
-
# class EncryptionWrapper
-
# def before_save(record)
-
# record.credit_card_number = encrypt(record.credit_card_number)
-
# end
-
#
-
# def after_save(record)
-
# record.credit_card_number = decrypt(record.credit_card_number)
-
# end
-
#
-
# alias_method :after_initialize, :after_save
-
#
-
# private
-
# def encrypt(value)
-
# # Secrecy is committed
-
# end
-
#
-
# def decrypt(value)
-
# # Secrecy is unveiled
-
# end
-
# end
-
#
-
# So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
-
# a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
-
# initialization data such as the name of the attribute to work with:
-
#
-
# class BankAccount < ActiveRecord::Base
-
# before_save EncryptionWrapper.new("credit_card_number")
-
# after_save EncryptionWrapper.new("credit_card_number")
-
# after_initialize EncryptionWrapper.new("credit_card_number")
-
# end
-
#
-
# class EncryptionWrapper
-
# def initialize(attribute)
-
# @attribute = attribute
-
# end
-
#
-
# def before_save(record)
-
# record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
-
# end
-
#
-
# def after_save(record)
-
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
-
# end
-
#
-
# alias_method :after_initialize, :after_save
-
#
-
# private
-
# def encrypt(value)
-
# # Secrecy is committed
-
# end
-
#
-
# def decrypt(value)
-
# # Secrecy is unveiled
-
# end
-
# end
-
#
-
# The callback macros usually accept a symbol for the method they're supposed to run, but you can also
-
# pass a "method string", which will then be evaluated within the binding of the callback. Example:
-
#
-
# class Topic < ActiveRecord::Base
-
# before_destroy 'self.class.delete_all "parent_id = #{id}"'
-
# end
-
#
-
# Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback
-
# is triggered. Also note that these inline callbacks can be stacked just like the regular ones:
-
#
-
# class Topic < ActiveRecord::Base
-
# before_destroy 'self.class.delete_all "parent_id = #{id}"',
-
# 'puts "Evaluated after parents are destroyed"'
-
# end
-
#
-
# == <tt>before_validation*</tt> returning statements
-
#
-
# If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be
-
# aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
-
# ActiveRecord::RecordInvalid exception. Nothing will be appended to the errors object.
-
#
-
# == Canceling callbacks
-
#
-
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are
-
# cancelled.
-
# Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
-
# methods on the model, which are called last.
-
#
-
# == Ordering callbacks
-
#
-
# Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
-
# callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option.
-
#
-
# Let's look at the code below:
-
#
-
# class Topic < ActiveRecord::Base
-
# has_many :children, dependent: destroy
-
#
-
# before_destroy :log_children
-
#
-
# private
-
# def log_children
-
# # Child processing
-
# end
-
# end
-
#
-
# In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
-
# because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
-
#
-
# class Topic < ActiveRecord::Base
-
# has_many :children, dependent: destroy
-
#
-
# before_destroy :log_children, prepend: true
-
#
-
# private
-
# def log_children
-
# # Child processing
-
# end
-
# end
-
#
-
# This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available.
-
#
-
# == Transactions
-
#
-
# The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
-
# within a transaction. That includes <tt>after_*</tt> hooks. If everything
-
# goes fine a COMMIT is executed once the chain has been completed.
-
#
-
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
-
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
-
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
-
# needs to be aware of it because an ordinary +save+ will raise such exception
-
# instead of quietly returning +false+.
-
#
-
# == Debugging callbacks
-
#
-
# The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
-
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
-
# defines what part of the chain the callback runs in.
-
#
-
# To find all callbacks in the before_save callback chain:
-
#
-
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
-
#
-
# Returns an array of callback objects that form the before_save chain.
-
#
-
# To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
-
#
-
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
-
#
-
# Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
-
#
-
2
module Callbacks
-
2
extend ActiveSupport::Concern
-
-
2
CALLBACKS = [
-
:after_initialize, :after_find, :after_touch, :before_validation, :after_validation,
-
:before_save, :around_save, :after_save, :before_create, :around_create,
-
:after_create, :before_update, :around_update, :after_update,
-
:before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
-
]
-
-
2
module ClassMethods
-
2
include ActiveModel::Callbacks
-
end
-
-
2
included do
-
2
include ActiveModel::Validations::Callbacks
-
-
2
define_model_callbacks :initialize, :find, :touch, :only => :after
-
2
define_model_callbacks :save, :create, :update, :destroy
-
end
-
-
2
def destroy #:nodoc:
-
6
_run_destroy_callbacks { super }
-
end
-
-
2
def touch(*) #:nodoc:
-
_run_touch_callbacks { super }
-
end
-
-
2
private
-
-
2
def create_or_update #:nodoc:
-
20
_run_save_callbacks { super }
-
end
-
-
2
def _create_record #:nodoc:
-
12
_run_create_callbacks { super }
-
end
-
-
2
def _update_record(*) #:nodoc:
-
8
_run_update_callbacks { super }
-
end
-
end
-
end
-
2
require 'thread'
-
2
require 'thread_safe'
-
2
require 'monitor'
-
2
require 'set'
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
# Raised when a connection could not be obtained within the connection
-
# acquisition timeout period: because max connections in pool
-
# are in use.
-
2
class ConnectionTimeoutError < ConnectionNotEstablished
-
end
-
-
2
module ConnectionAdapters
-
# Connection pool base class for managing Active Record database
-
# connections.
-
#
-
# == Introduction
-
#
-
# A connection pool synchronizes thread access to a limited number of
-
# database connections. The basic idea is that each thread checks out a
-
# database connection from the pool, uses that connection, and checks the
-
# connection back in. ConnectionPool is completely thread-safe, and will
-
# ensure that a connection cannot be used by two threads at the same time,
-
# as long as ConnectionPool's contract is correctly followed. It will also
-
# handle cases in which there are more threads than connections: if all
-
# connections have been checked out, and a thread tries to checkout a
-
# connection anyway, then ConnectionPool will wait until some other thread
-
# has checked in a connection.
-
#
-
# == Obtaining (checking out) a connection
-
#
-
# Connections can be obtained and used from a connection pool in several
-
# ways:
-
#
-
# 1. Simply use ActiveRecord::Base.connection as with Active Record 2.1 and
-
# earlier (pre-connection-pooling). Eventually, when you're done with
-
# the connection(s) and wish it to be returned to the pool, you call
-
# ActiveRecord::Base.clear_active_connections!. This will be the
-
# default behavior for Active Record when used in conjunction with
-
# Action Pack's request handling cycle.
-
# 2. Manually check out a connection from the pool with
-
# ActiveRecord::Base.connection_pool.checkout. You are responsible for
-
# returning this connection to the pool when finished by calling
-
# ActiveRecord::Base.connection_pool.checkin(connection).
-
# 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
-
# obtains a connection, yields it as the sole argument to the block,
-
# and returns it to the pool after the block completes.
-
#
-
# Connections in the pool are actually AbstractAdapter objects (or objects
-
# compatible with AbstractAdapter's interface).
-
#
-
# == Options
-
#
-
# There are several connection-pooling-related options that you can add to
-
# your database connection configuration:
-
#
-
# * +pool+: number indicating size of connection pool (default 5)
-
# * +checkout_timeout+: number of seconds to block and wait for a connection
-
# before giving up and raising a timeout error (default 5 seconds).
-
# * +reaping_frequency+: frequency in seconds to periodically run the
-
# Reaper, which attempts to find and recover connections from dead
-
# threads, which can occur if a programmer forgets to close a
-
# connection at the end of a thread or a thread dies unexpectedly.
-
# Regardless of this setting, the Reaper will be invoked before every
-
# blocking wait. (Default nil, which means don't schedule the Reaper).
-
2
class ConnectionPool
-
# Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
-
# with which it shares a Monitor. But could be a generic Queue.
-
#
-
# The Queue in stdlib's 'thread' could replace this class except
-
# stdlib's doesn't support waiting with a timeout.
-
2
class Queue
-
2
def initialize(lock = Monitor.new)
-
2
@lock = lock
-
2
@cond = @lock.new_cond
-
2
@num_waiting = 0
-
2
@queue = []
-
end
-
-
# Test if any threads are currently waiting on the queue.
-
2
def any_waiting?
-
synchronize do
-
@num_waiting > 0
-
end
-
end
-
-
# Returns the number of threads currently waiting on this
-
# queue.
-
2
def num_waiting
-
synchronize do
-
@num_waiting
-
end
-
end
-
-
# Add +element+ to the queue. Never blocks.
-
2
def add(element)
-
39
synchronize do
-
39
@queue.push element
-
39
@cond.signal
-
end
-
end
-
-
# If +element+ is in the queue, remove and return it, or nil.
-
2
def delete(element)
-
synchronize do
-
@queue.delete(element)
-
end
-
end
-
-
# Remove all elements from the queue.
-
2
def clear
-
synchronize do
-
@queue.clear
-
end
-
end
-
-
# Remove the head of the queue.
-
#
-
# If +timeout+ is not given, remove and return the head the
-
# queue if the number of available elements is strictly
-
# greater than the number of threads currently waiting (that
-
# is, don't jump ahead in line). Otherwise, return nil.
-
#
-
# If +timeout+ is given, block if it there is no element
-
# available, waiting up to +timeout+ seconds for an element to
-
# become available.
-
#
-
# Raises:
-
# - ConnectionTimeoutError if +timeout+ is given and no element
-
# becomes available after +timeout+ seconds,
-
2
def poll(timeout = nil)
-
39
synchronize do
-
39
if timeout
-
no_wait_poll || wait_poll(timeout)
-
else
-
39
no_wait_poll
-
end
-
end
-
end
-
-
2
private
-
-
2
def synchronize(&block)
-
78
@lock.synchronize(&block)
-
end
-
-
# Test if the queue currently contains any elements.
-
2
def any?
-
!@queue.empty?
-
end
-
-
# A thread can remove an element from the queue without
-
# waiting if an only if the number of currently available
-
# connections is strictly greater than the number of waiting
-
# threads.
-
2
def can_remove_no_wait?
-
39
@queue.size > @num_waiting
-
end
-
-
# Removes and returns the head of the queue if possible, or nil.
-
2
def remove
-
37
@queue.shift
-
end
-
-
# Remove and return the head the queue if the number of
-
# available elements is strictly greater than the number of
-
# threads currently waiting. Otherwise, return nil.
-
2
def no_wait_poll
-
39
remove if can_remove_no_wait?
-
end
-
-
# Waits on the queue up to +timeout+ seconds, then removes and
-
# returns the head of the queue.
-
2
def wait_poll(timeout)
-
@num_waiting += 1
-
-
t0 = Time.now
-
elapsed = 0
-
loop do
-
@cond.wait(timeout - elapsed)
-
-
return remove if any?
-
-
elapsed = Time.now - t0
-
if elapsed >= timeout
-
msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' %
-
[timeout, elapsed]
-
raise ConnectionTimeoutError, msg
-
end
-
end
-
ensure
-
@num_waiting -= 1
-
end
-
end
-
-
# Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
-
# A reaper instantiated with a nil frequency will never reap the
-
# connection pool.
-
#
-
# Configure the frequency by setting "reaping_frequency" in your
-
# database yaml file.
-
2
class Reaper
-
2
attr_reader :pool, :frequency
-
-
2
def initialize(pool, frequency)
-
2
@pool = pool
-
2
@frequency = frequency
-
end
-
-
2
def run
-
2
return unless frequency
-
Thread.new(frequency, pool) { |t, p|
-
while true
-
sleep t
-
p.reap
-
end
-
}
-
end
-
end
-
-
2
include MonitorMixin
-
-
2
attr_accessor :automatic_reconnect, :checkout_timeout
-
2
attr_reader :spec, :connections, :size, :reaper
-
-
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
-
# object which describes database connection information (e.g. adapter,
-
# host name, username, password, etc), as well as the maximum size for
-
# this ConnectionPool.
-
#
-
# The default ConnectionPool maximum size is 5.
-
2
def initialize(spec)
-
2
super()
-
-
2
@spec = spec
-
-
2
@checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5
-
2
@reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f))
-
2
@reaper.run
-
-
# default max pool size to 5
-
2
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
-
-
# The cache of reserved connections mapped to threads
-
2
@reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
-
-
2
@connections = []
-
2
@automatic_reconnect = true
-
-
2
@available = Queue.new self
-
end
-
-
# Retrieve the connection associated with the current thread, or call
-
# #checkout to obtain one if necessary.
-
#
-
# #connection can be called any number of times; the connection is
-
# held in a hash keyed by the thread id.
-
2
def connection
-
# this is correctly done double-checked locking
-
# (ThreadSafe::Cache's lookups have volatile semantics)
-
@reserved_connections[current_connection_id] || synchronize do
-
39
@reserved_connections[current_connection_id] ||= checkout
-
685
end
-
end
-
-
# Is there an open connection that is being used for the current thread?
-
2
def active_connection?
-
synchronize do
-
@reserved_connections.fetch(current_connection_id) {
-
return false
-
}.in_use?
-
end
-
end
-
-
# Signal that the thread is finished with the current connection.
-
# #release_connection releases the connection-thread association
-
# and returns the connection to the pool.
-
2
def release_connection(with_id = current_connection_id)
-
39
synchronize do
-
39
conn = @reserved_connections.delete(with_id)
-
39
checkin conn if conn
-
end
-
end
-
-
# If a connection already exists yield it to the block. If no connection
-
# exists checkout a connection, yield it to the block, and checkin the
-
# connection when finished.
-
2
def with_connection
-
connection_id = current_connection_id
-
fresh_connection = true unless active_connection?
-
yield connection
-
ensure
-
release_connection(connection_id) if fresh_connection
-
end
-
-
# Returns true if a connection has already been opened.
-
2
def connected?
-
94
synchronize { @connections.any? }
-
end
-
-
# Disconnects all connections in the pool, and clears the pool.
-
2
def disconnect!
-
synchronize do
-
@reserved_connections.clear
-
@connections.each do |conn|
-
checkin conn
-
conn.disconnect!
-
end
-
@connections = []
-
@available.clear
-
end
-
end
-
-
# Clears the cache which maps classes.
-
2
def clear_reloadable_connections!
-
synchronize do
-
@reserved_connections.clear
-
@connections.each do |conn|
-
checkin conn
-
conn.disconnect! if conn.requires_reloading?
-
end
-
@connections.delete_if do |conn|
-
conn.requires_reloading?
-
end
-
@available.clear
-
@connections.each do |conn|
-
@available.add conn
-
end
-
end
-
end
-
-
# Check-out a database connection from the pool, indicating that you want
-
# to use it. You should call #checkin when you no longer need this.
-
#
-
# This is done by either returning and leasing existing connection, or by
-
# creating a new connection and leasing it.
-
#
-
# If all connections are leased and the pool is at capacity (meaning the
-
# number of currently leased connections is greater than or equal to the
-
# size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised.
-
#
-
# Returns: an AbstractAdapter object.
-
#
-
# Raises:
-
# - ConnectionTimeoutError: no connection can be obtained from the pool.
-
2
def checkout
-
39
synchronize do
-
39
conn = acquire_connection
-
39
conn.lease
-
39
checkout_and_verify(conn)
-
end
-
end
-
-
# Check-in a database connection back into the pool, indicating that you
-
# no longer need this connection.
-
#
-
# +conn+: an AbstractAdapter object, which was obtained by earlier by
-
# calling +checkout+ on this pool.
-
2
def checkin(conn)
-
39
synchronize do
-
39
owner = conn.owner
-
-
39
conn._run_checkin_callbacks do
-
39
conn.expire
-
end
-
-
39
release conn, owner
-
-
39
@available.add conn
-
end
-
end
-
-
# Remove a connection from the connection pool. The connection will
-
# remain open and active but will no longer be managed by this pool.
-
2
def remove(conn)
-
synchronize do
-
@connections.delete conn
-
@available.delete conn
-
-
release conn, conn.owner
-
-
@available.add checkout_new_connection if @available.any_waiting?
-
end
-
end
-
-
# Recover lost connections for the pool. A lost connection can occur if
-
# a programmer forgets to checkin a connection at the end of a thread
-
# or a thread dies unexpectedly.
-
2
def reap
-
stale_connections = synchronize do
-
@connections.select do |conn|
-
conn.in_use? && !conn.owner.alive?
-
end
-
end
-
-
stale_connections.each do |conn|
-
synchronize do
-
if conn.active?
-
conn.reset!
-
checkin conn
-
else
-
remove conn
-
end
-
end
-
end
-
end
-
-
2
private
-
-
# Acquire a connection by one of 1) immediately removing one
-
# from the queue of available connections, 2) creating a new
-
# connection if the pool is not at capacity, 3) waiting on the
-
# queue for a connection to become available.
-
#
-
# Raises:
-
# - ConnectionTimeoutError if a connection could not be acquired
-
2
def acquire_connection
-
39
if conn = @available.poll
-
37
conn
-
2
elsif @connections.size < @size
-
2
checkout_new_connection
-
else
-
reap
-
@available.poll(@checkout_timeout)
-
end
-
end
-
-
2
def release(conn, owner)
-
39
thread_id = owner.object_id
-
-
39
if @reserved_connections[thread_id] == conn
-
@reserved_connections.delete thread_id
-
end
-
end
-
-
2
def new_connection
-
2
Base.send(spec.adapter_method, spec.config)
-
end
-
-
2
def current_connection_id #:nodoc:
-
763
Base.connection_id ||= Thread.current.object_id
-
end
-
-
2
def checkout_new_connection
-
2
raise ConnectionNotEstablished unless @automatic_reconnect
-
-
2
c = new_connection
-
2
c.pool = self
-
2
@connections << c
-
2
c
-
end
-
-
2
def checkout_and_verify(c)
-
39
c._run_checkout_callbacks do
-
39
c.verify!
-
end
-
39
c
-
rescue
-
remove c
-
c.disconnect!
-
raise
-
end
-
end
-
-
# ConnectionHandler is a collection of ConnectionPool objects. It is used
-
# for keeping separate connection pools for Active Record models that connect
-
# to different databases.
-
#
-
# For example, suppose that you have 5 models, with the following hierarchy:
-
#
-
# class Author < ActiveRecord::Base
-
# end
-
#
-
# class BankAccount < ActiveRecord::Base
-
# end
-
#
-
# class Book < ActiveRecord::Base
-
# establish_connection "library_db"
-
# end
-
#
-
# class ScaryBook < Book
-
# end
-
#
-
# class GoodBook < Book
-
# end
-
#
-
# And a database.yml that looked like this:
-
#
-
# development:
-
# database: my_application
-
# host: localhost
-
#
-
# library_db:
-
# database: library
-
# host: some.library.org
-
#
-
# Your primary database in the development environment is "my_application"
-
# but the Book model connects to a separate database called "library_db"
-
# (this can even be a database on a different machine).
-
#
-
# Book, ScaryBook and GoodBook will all use the same connection pool to
-
# "library_db" while Author, BankAccount, and any other models you create
-
# will use the default connection pool to "my_application".
-
#
-
# The various connection pools are managed by a single instance of
-
# ConnectionHandler accessible via ActiveRecord::Base.connection_handler.
-
# All Active Record models use this handler to determine the connection pool that they
-
# should use.
-
2
class ConnectionHandler
-
2
def initialize
-
# These caches are keyed by klass.name, NOT klass. Keying them by klass
-
# alone would lead to memory leaks in development mode as all previous
-
# instances of the class would stay in memory.
-
2
@owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
-
2
h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
-
end
-
2
@class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
-
2
h[k] = ThreadSafe::Cache.new
-
end
-
end
-
-
2
def connection_pool_list
-
78
owner_to_pool.values.compact
-
end
-
-
2
def connection_pools
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
In the next release, this will return the same as `#connection_pool_list`.
-
(An array of pools, rather than a hash mapping specs to pools.)
-
MSG
-
-
Hash[connection_pool_list.map { |pool| [pool.spec, pool] }]
-
end
-
-
2
def establish_connection(owner, spec)
-
2
@class_to_pool.clear
-
2
raise RuntimeError, "Anonymous class is not allowed." unless owner.name
-
2
owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
-
end
-
-
# Returns true if there are any active connections among the connection
-
# pools that the ConnectionHandler is managing.
-
2
def active_connections?
-
connection_pool_list.any?(&:active_connection?)
-
end
-
-
# Returns any connections in use by the current thread back to the pool,
-
# and also returns connections to the pool cached by threads that are no
-
# longer alive.
-
2
def clear_active_connections!
-
39
connection_pool_list.each(&:release_connection)
-
end
-
-
# Clears the cache which maps classes.
-
2
def clear_reloadable_connections!
-
connection_pool_list.each(&:clear_reloadable_connections!)
-
end
-
-
2
def clear_all_connections!
-
connection_pool_list.each(&:disconnect!)
-
end
-
-
# Locate the connection of the nearest super class. This can be an
-
# active or defined connection: if it is the latter, it will be
-
# opened and set as the active connection for the class it was defined
-
# for (not necessarily the current class).
-
2
def retrieve_connection(klass) #:nodoc:
-
646
pool = retrieve_connection_pool(klass)
-
646
raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
-
646
conn = pool.connection
-
646
raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
-
646
conn
-
end
-
-
# Returns true if a connection that's accessible to this class has
-
# already been opened.
-
2
def connected?(klass)
-
47
conn = retrieve_connection_pool(klass)
-
47
conn && conn.connected?
-
end
-
-
# Remove the connection for this class. This will close the active
-
# connection and the defined connection (if they exist). The result
-
# can be used as an argument for establish_connection, for easily
-
# re-establishing the connection.
-
2
def remove_connection(owner)
-
2
if pool = owner_to_pool.delete(owner.name)
-
@class_to_pool.clear
-
pool.automatic_reconnect = false
-
pool.disconnect!
-
pool.spec.config
-
end
-
end
-
-
# Retrieving the connection pool happens a lot so we cache it in @class_to_pool.
-
# This makes retrieving the connection pool O(1) once the process is warm.
-
# When a connection is established or removed, we invalidate the cache.
-
#
-
# Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
-
# However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that
-
# #fetch is significantly slower than #[]. So in the nil case, no caching will
-
# take place, but that's ok since the nil case is not the common one that we wish
-
# to optimise for.
-
2
def retrieve_connection_pool(klass)
-
705
class_to_pool[klass.name] ||= begin
-
14
until pool = pool_for(klass)
-
12
klass = klass.superclass
-
12
break unless klass <= Base
-
end
-
-
14
class_to_pool[klass.name] = pool
-
end
-
end
-
-
2
private
-
-
2
def owner_to_pool
-
120
@owner_to_pool[Process.pid]
-
end
-
-
2
def class_to_pool
-
719
@class_to_pool[Process.pid]
-
end
-
-
2
def pool_for(owner)
-
26
owner_to_pool.fetch(owner.name) {
-
12
if ancestor_pool = pool_from_any_process_for(owner)
-
# A connection was established in an ancestor process that must have
-
# subsequently forked. We can't reuse the connection, but we can copy
-
# the specification and establish a new connection with it.
-
establish_connection owner, ancestor_pool.spec
-
else
-
12
owner_to_pool[owner.name] = nil
-
end
-
}
-
end
-
-
2
def pool_from_any_process_for(owner)
-
24
owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] }
-
12
owner_to_pool && owner_to_pool[owner.name]
-
end
-
end
-
-
2
class ConnectionManagement
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
testing = env['rack.test']
-
-
response = @app.call(env)
-
response[2] = ::Rack::BodyProxy.new(response[2]) do
-
ActiveRecord::Base.clear_active_connections! unless testing
-
end
-
-
response
-
rescue Exception
-
ActiveRecord::Base.clear_active_connections! unless testing
-
raise
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module ConnectionAdapters
-
2
module Savepoints #:nodoc:
-
2
def supports_savepoints?
-
true
-
end
-
-
2
def create_savepoint(name = current_savepoint_name)
-
17
execute("SAVEPOINT #{name}")
-
end
-
-
2
def exec_rollback_to_savepoint(name = current_savepoint_name)
-
4
execute("ROLLBACK TO SAVEPOINT #{name}")
-
end
-
-
2
def release_savepoint(name = current_savepoint_name)
-
13
execute("RELEASE SAVEPOINT #{name}")
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module ConnectionAdapters
-
2
class TransactionState
-
2
attr_reader :parent
-
-
2
VALID_STATES = Set.new([:committed, :rolledback, nil])
-
-
2
def initialize(state = nil)
-
58
@state = state
-
58
@parent = nil
-
end
-
-
2
def finalized?
-
84
@state
-
end
-
-
2
def committed?
-
23
@state == :committed
-
end
-
-
2
def rolledback?
-
32
@state == :rolledback
-
end
-
-
2
def completed?
-
committed? || rolledback?
-
end
-
-
2
def set_state(state)
-
58
if !VALID_STATES.include?(state)
-
raise ArgumentError, "Invalid transaction state: #{state}"
-
end
-
58
@state = state
-
end
-
end
-
-
2
class NullTransaction #:nodoc:
-
2
def initialize; end
-
2
def closed?; true; end
-
2
def open?; false; end
-
2
def joinable?; false; end
-
2
def add_record(record); end
-
end
-
-
2
class Transaction #:nodoc:
-
-
2
attr_reader :connection, :state, :records, :savepoint_name
-
2
attr_writer :joinable
-
-
2
def initialize(connection, options)
-
58
@connection = connection
-
58
@state = TransactionState.new
-
58
@records = []
-
58
@joinable = options.fetch(:joinable, true)
-
end
-
-
2
def add_record(record)
-
6
records << record
-
end
-
-
2
def rollback
-
43
@state.set_state(:rolledback)
-
end
-
-
2
def rollback_records
-
43
ite = records.uniq
-
43
while record = ite.shift
-
3
begin
-
3
record.rolledback! full_rollback?
-
rescue => e
-
raise if ActiveRecord::Base.raise_in_transactional_callbacks
-
record.logger.error(e) if record.respond_to?(:logger) && record.logger
-
end
-
end
-
ensure
-
43
ite.each do |i|
-
i.rolledback!(full_rollback?, false)
-
end
-
end
-
-
2
def commit
-
15
@state.set_state(:committed)
-
end
-
-
2
def commit_records
-
2
ite = records.uniq
-
2
while record = ite.shift
-
begin
-
record.committed!
-
rescue => e
-
raise if ActiveRecord::Base.raise_in_transactional_callbacks
-
record.logger.error(e) if record.respond_to?(:logger) && record.logger
-
end
-
end
-
ensure
-
2
ite.each do |i|
-
i.committed!(false)
-
end
-
end
-
-
5
def full_rollback?; true; end
-
25
def joinable?; @joinable; end
-
41
def closed?; false; end
-
41
def open?; !closed?; end
-
end
-
-
2
class SavepointTransaction < Transaction
-
-
2
def initialize(connection, savepoint_name, options)
-
17
super(connection, options)
-
17
if options[:isolation]
-
raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
-
end
-
17
connection.create_savepoint(@savepoint_name = savepoint_name)
-
end
-
-
2
def rollback
-
4
connection.rollback_to_savepoint(savepoint_name)
-
4
super
-
4
rollback_records
-
end
-
-
2
def commit
-
13
connection.release_savepoint(savepoint_name)
-
13
super
-
13
parent = connection.transaction_manager.current_transaction
-
16
records.each { |r| parent.add_record(r) }
-
end
-
-
2
def full_rollback?; false; end
-
end
-
-
2
class RealTransaction < Transaction
-
-
2
def initialize(connection, options)
-
41
super
-
41
if options[:isolation]
-
connection.begin_isolated_db_transaction(options[:isolation])
-
else
-
41
connection.begin_db_transaction
-
end
-
end
-
-
2
def rollback
-
39
connection.rollback_db_transaction
-
39
super
-
39
rollback_records
-
end
-
-
2
def commit
-
2
connection.commit_db_transaction
-
2
super
-
2
commit_records
-
end
-
end
-
-
2
class TransactionManager #:nodoc:
-
2
def initialize(connection)
-
2
@stack = []
-
2
@connection = connection
-
end
-
-
2
def begin_transaction(options = {})
-
58
transaction =
-
if @stack.empty?
-
41
RealTransaction.new(@connection, options)
-
else
-
17
SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options)
-
end
-
58
@stack.push(transaction)
-
58
transaction
-
end
-
-
2
def commit_transaction
-
15
@stack.pop.commit
-
end
-
-
2
def rollback_transaction
-
43
@stack.pop.rollback
-
end
-
-
2
def within_new_transaction(options = {})
-
19
transaction = begin_transaction options
-
19
yield
-
rescue Exception => error
-
4
rollback_transaction if transaction
-
4
raise
-
ensure
-
19
unless error
-
15
if Thread.current.status == 'aborting'
-
rollback_transaction if transaction
-
else
-
15
begin
-
15
commit_transaction
-
rescue Exception
-
transaction.rollback unless transaction.state.completed?
-
raise
-
end
-
end
-
end
-
end
-
-
2
def open_transactions
-
@stack.size
-
end
-
-
2
def current_transaction
-
101
@stack.last || NULL_TRANSACTION
-
end
-
-
2
private
-
2
NULL_TRANSACTION = NullTransaction.new
-
end
-
end
-
end
-
2
require 'set'
-
-
2
module ActiveRecord
-
# :stopdoc:
-
2
module ConnectionAdapters
-
# An abstract definition of a column in a table.
-
2
class Column
-
2
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON'].to_set
-
2
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
-
-
2
module Format
-
2
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
-
2
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
-
end
-
-
2
attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
-
-
2
delegate :type, :precision, :scale, :limit, :klass, :accessor,
-
:text?, :number?, :binary?, :changed?,
-
:type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
-
:type_cast_for_schema,
-
to: :cast_type
-
-
# Instantiates a new column in the table.
-
#
-
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
-
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
-
# +cast_type+ is the object used for type casting and type information.
-
# +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
-
# <tt>company_name varchar(60)</tt>.
-
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
-
# +null+ determines if this column allows +NULL+ values.
-
2
def initialize(name, default, cast_type, sql_type = nil, null = true)
-
174
@name = name.freeze
-
174
@cast_type = cast_type
-
174
@sql_type = sql_type
-
174
@null = null
-
174
@default = default
-
174
@default_function = nil
-
end
-
-
2
def has_default?
-
!default.nil?
-
end
-
-
# Returns the human name of the column name.
-
#
-
# ===== Examples
-
# Column.new('sales_stage', ...).human_name # => 'Sales stage'
-
2
def human_name
-
Base.human_attribute_name(@name)
-
end
-
-
2
def with_type(type)
-
88
dup.tap do |clone|
-
88
clone.instance_variable_set('@cast_type', type)
-
end
-
end
-
-
2
def ==(other)
-
other.name == name &&
-
other.default == default &&
-
other.cast_type == cast_type &&
-
other.sql_type == sql_type &&
-
other.null == null &&
-
other.default_function == default_function
-
end
-
2
alias :eql? :==
-
-
2
def hash
-
attributes_for_hash.hash
-
end
-
-
2
private
-
-
2
def attributes_for_hash
-
[self.class, name, default, cast_type, sql_type, null, default_function]
-
end
-
end
-
end
-
# :startdoc:
-
end
-
2
require 'uri'
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
2
module ConnectionAdapters
-
2
class ConnectionSpecification #:nodoc:
-
2
attr_reader :config, :adapter_method
-
-
2
def initialize(config, adapter_method)
-
2
@config, @adapter_method = config, adapter_method
-
end
-
-
2
def initialize_dup(original)
-
@config = original.config.dup
-
end
-
-
# Expands a connection string into a hash.
-
2
class ConnectionUrlResolver # :nodoc:
-
-
# == Example
-
#
-
# url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
-
# ConnectionUrlResolver.new(url).to_hash
-
# # => {
-
# "adapter" => "postgresql",
-
# "host" => "localhost",
-
# "port" => 9000,
-
# "database" => "foo_test",
-
# "username" => "foo",
-
# "password" => "bar",
-
# "pool" => "5",
-
# "timeout" => "3000"
-
# }
-
2
def initialize(url)
-
raise "Database URL cannot be empty" if url.blank?
-
@uri = uri_parser.parse(url)
-
@adapter = @uri.scheme.tr('-', '_')
-
@adapter = "postgresql" if @adapter == "postgres"
-
-
if @uri.opaque
-
@uri.opaque, @query = @uri.opaque.split('?', 2)
-
else
-
@query = @uri.query
-
end
-
end
-
-
# Converts the given URL to a full connection hash.
-
2
def to_hash
-
config = raw_config.reject { |_,value| value.blank? }
-
config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String }
-
config
-
end
-
-
2
private
-
-
2
def uri
-
@uri
-
end
-
-
2
def uri_parser
-
@uri_parser ||= URI::Parser.new
-
end
-
-
# Converts the query parameters of the URI into a hash.
-
#
-
# "localhost?pool=5&reaping_frequency=2"
-
# # => { "pool" => "5", "reaping_frequency" => "2" }
-
#
-
# returns empty hash if no query present.
-
#
-
# "localhost"
-
# # => {}
-
2
def query_hash
-
Hash[(@query || '').split("&").map { |pair| pair.split("=") }]
-
end
-
-
2
def raw_config
-
if uri.opaque
-
query_hash.merge({
-
"adapter" => @adapter,
-
"database" => uri.opaque })
-
else
-
query_hash.merge({
-
"adapter" => @adapter,
-
"username" => uri.user,
-
"password" => uri.password,
-
"port" => uri.port,
-
"database" => database_from_path,
-
"host" => uri.hostname })
-
end
-
end
-
-
# Returns name of the database.
-
2
def database_from_path
-
if @adapter == 'sqlite3'
-
# 'sqlite3:/foo' is absolute, because that makes sense. The
-
# corresponding relative version, 'sqlite3:foo', is handled
-
# elsewhere, as an "opaque".
-
-
uri.path
-
else
-
# Only SQLite uses a filename as the "database" name; for
-
# anything else, a leading slash would be silly.
-
-
uri.path.sub(%r{^/}, "")
-
end
-
end
-
end
-
-
##
-
# Builds a ConnectionSpecification from user input.
-
2
class Resolver # :nodoc:
-
2
attr_reader :configurations
-
-
# Accepts a hash two layers deep, keys on the first layer represent
-
# environments such as "production". Keys must be strings.
-
2
def initialize(configurations)
-
6
@configurations = configurations
-
end
-
-
# Returns a hash with database connection information.
-
#
-
# == Examples
-
#
-
# Full hash Configuration.
-
#
-
# configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
-
# Resolver.new(configurations).resolve(:production)
-
# # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3"}
-
#
-
# Initialized with URL configuration strings.
-
#
-
# configurations = { "production" => "postgresql://localhost/foo" }
-
# Resolver.new(configurations).resolve(:production)
-
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
-
#
-
2
def resolve(config)
-
10
if config
-
10
resolve_connection config
-
elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
-
resolve_symbol_connection env.to_sym
-
else
-
raise AdapterNotSpecified
-
end
-
end
-
-
# Expands each key in @configurations hash into fully resolved hash
-
2
def resolve_all
-
4
config = configurations.dup
-
4
config.each do |key, value|
-
8
config[key] = resolve(value) if value
-
end
-
4
config
-
end
-
-
# Returns an instance of ConnectionSpecification for a given adapter.
-
# Accepts a hash one layer deep that contains all connection information.
-
#
-
# == Example
-
#
-
# config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
-
# spec = Resolver.new(config).spec(:production)
-
# spec.adapter_method
-
# # => "sqlite3_connection"
-
# spec.config
-
# # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
-
#
-
2
def spec(config)
-
2
spec = resolve(config).symbolize_keys
-
-
2
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
-
-
2
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
-
2
begin
-
2
require path_to_adapter
-
rescue Gem::LoadError => e
-
raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
-
rescue LoadError => e
-
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
-
end
-
-
2
adapter_method = "#{spec[:adapter]}_connection"
-
2
ConnectionSpecification.new(spec, adapter_method)
-
end
-
-
2
private
-
-
# Returns fully resolved connection, accepts hash, string or symbol.
-
# Always returns a hash.
-
#
-
# == Examples
-
#
-
# Symbol representing current environment.
-
#
-
# Resolver.new("production" => {}).resolve_connection(:production)
-
# # => {}
-
#
-
# One layer deep hash of connection values.
-
#
-
# Resolver.new({}).resolve_connection("adapter" => "sqlite3")
-
# # => { "adapter" => "sqlite3" }
-
#
-
# Connection URL.
-
#
-
# Resolver.new({}).resolve_connection("postgresql://localhost/foo")
-
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
-
#
-
2
def resolve_connection(spec)
-
12
case spec
-
when Symbol
-
2
resolve_symbol_connection spec
-
when String
-
resolve_string_connection spec
-
when Hash
-
10
resolve_hash_connection spec
-
end
-
end
-
-
2
def resolve_string_connection(spec)
-
# Rails has historically accepted a string to mean either
-
# an environment key or a URL spec, so we have deprecated
-
# this ambiguous behaviour and in the future this function
-
# can be removed in favor of resolve_url_connection.
-
if configurations.key?(spec) || spec !~ /:/
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
Passing a string to ActiveRecord::Base.establish_connection for a
-
configuration lookup is deprecated, please pass a symbol
-
(#{spec.to_sym.inspect}) instead.
-
MSG
-
-
resolve_symbol_connection(spec)
-
else
-
resolve_url_connection(spec)
-
end
-
end
-
-
# Takes the environment such as +:production+ or +:development+.
-
# This requires that the @configurations was initialized with a key that
-
# matches.
-
#
-
# Resolver.new("production" => {}).resolve_symbol_connection(:production)
-
# # => {}
-
#
-
2
def resolve_symbol_connection(spec)
-
2
if config = configurations[spec.to_s]
-
2
resolve_connection(config)
-
else
-
raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
-
end
-
end
-
-
# Accepts a hash. Expands the "url" key that contains a
-
# URL database connection to a full connection
-
# hash and merges with the rest of the hash.
-
# Connection details inside of the "url" key win any merge conflicts
-
2
def resolve_hash_connection(spec)
-
10
if spec["url"] && spec["url"] !~ /^jdbc:/
-
connection_hash = resolve_url_connection(spec.delete("url"))
-
spec.merge!(connection_hash)
-
end
-
10
spec
-
end
-
-
# Takes a connection URL.
-
#
-
# Resolver.new({}).resolve_url_connection("postgresql://localhost/foo")
-
# # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
-
#
-
2
def resolve_url_connection(url)
-
ConnectionUrlResolver.new(url).to_hash
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_record/connection_adapters/abstract_adapter'
-
2
require 'active_record/connection_adapters/statement_pool'
-
2
require 'arel/visitors/bind_visitor'
-
-
2
gem 'sqlite3', '~> 1.3.6'
-
2
require 'sqlite3'
-
-
2
module ActiveRecord
-
2
module ConnectionHandling # :nodoc:
-
# sqlite3 adapter reuses sqlite_connection.
-
2
def sqlite3_connection(config)
-
# Require database.
-
2
unless config[:database]
-
raise ArgumentError, "No database file specified. Missing argument: database"
-
end
-
-
# Allow database path relative to Rails.root, but only if the database
-
# path is not the special path that tells sqlite to build a database only
-
# in memory.
-
2
if ':memory:' != config[:database]
-
2
config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
-
2
dirname = File.dirname(config[:database])
-
2
Dir.mkdir(dirname) unless File.directory?(dirname)
-
end
-
-
2
db = SQLite3::Database.new(
-
config[:database].to_s,
-
:results_as_hash => true
-
)
-
-
2
db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
-
-
2
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
-
rescue Errno::ENOENT => error
-
if error.message.include?("No such file or directory")
-
raise ActiveRecord::NoDatabaseError.new(error.message, error)
-
else
-
raise
-
end
-
end
-
end
-
-
2
module ConnectionAdapters #:nodoc:
-
2
class SQLite3Binary < Type::Binary # :nodoc:
-
2
def cast_value(value)
-
if value.encoding != Encoding::ASCII_8BIT
-
value = value.force_encoding(Encoding::ASCII_8BIT)
-
end
-
value
-
end
-
end
-
-
# The SQLite3 adapter works SQLite 3.6.16 or newer
-
# with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3).
-
#
-
# Options:
-
#
-
# * <tt>:database</tt> - Path to the database file.
-
2
class SQLite3Adapter < AbstractAdapter
-
2
ADAPTER_NAME = 'SQLite'.freeze
-
2
include Savepoints
-
-
2
NATIVE_DATABASE_TYPES = {
-
primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
-
string: { name: "varchar" },
-
text: { name: "text" },
-
integer: { name: "integer" },
-
float: { name: "float" },
-
decimal: { name: "decimal" },
-
datetime: { name: "datetime" },
-
time: { name: "time" },
-
date: { name: "date" },
-
binary: { name: "blob" },
-
boolean: { name: "boolean" }
-
}
-
-
2
class Version
-
2
include Comparable
-
-
2
def initialize(version_string)
-
@version = version_string.split('.').map { |v| v.to_i }
-
end
-
-
2
def <=>(version_string)
-
@version <=> version_string.split('.').map { |v| v.to_i }
-
end
-
end
-
-
2
class StatementPool < ConnectionAdapters::StatementPool
-
2
def initialize(connection, max)
-
2
super
-
4
@cache = Hash.new { |h,pid| h[pid] = {} }
-
end
-
-
2
def each(&block); cache.each(&block); end
-
2
def key?(key); cache.key?(key); end
-
131
def [](key); cache[key]; end
-
2
def length; cache.length; end
-
-
2
def []=(sql, key)
-
20
while @max <= cache.size
-
dealloc(cache.shift.last[:stmt])
-
end
-
20
cache[sql] = key
-
end
-
-
2
def clear
-
cache.each_value do |hash|
-
dealloc hash[:stmt]
-
end
-
cache.clear
-
end
-
-
2
private
-
2
def cache
-
169
@cache[$$]
-
end
-
-
2
def dealloc(stmt)
-
stmt.close unless stmt.closed?
-
end
-
end
-
-
2
def initialize(connection, logger, connection_options, config)
-
2
super(connection, logger)
-
-
2
@active = nil
-
2
@statements = StatementPool.new(@connection,
-
2
self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
-
2
@config = config
-
-
2
@visitor = Arel::Visitors::SQLite.new self
-
-
4
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
-
2
@prepared_statements = true
-
else
-
@prepared_statements = false
-
end
-
end
-
-
2
def supports_ddl_transactions?
-
true
-
end
-
-
2
def supports_savepoints?
-
true
-
end
-
-
2
def supports_partial_index?
-
sqlite_version >= '3.8.0'
-
end
-
-
# Returns true, since this connection adapter supports prepared statement
-
# caching.
-
2
def supports_statement_cache?
-
true
-
end
-
-
# Returns true, since this connection adapter supports migrations.
-
2
def supports_migrations? #:nodoc:
-
true
-
end
-
-
2
def supports_primary_key? #:nodoc:
-
true
-
end
-
-
2
def requires_reloading?
-
true
-
end
-
-
2
def supports_views?
-
true
-
end
-
-
2
def active?
-
39
@active != false
-
end
-
-
# Disconnects from the database if already connected. Otherwise, this
-
# method does nothing.
-
2
def disconnect!
-
super
-
@active = false
-
@connection.close rescue nil
-
end
-
-
# Clears the prepared statements cache.
-
2
def clear_cache!
-
@statements.clear
-
end
-
-
2
def supports_index_sort_order?
-
true
-
end
-
-
# Returns 62. SQLite supports index names up to 64
-
# characters. The rest is used by rails internally to perform
-
# temporary rename operations
-
2
def allowed_index_name_length
-
index_name_length - 2
-
end
-
-
2
def native_database_types #:nodoc:
-
NATIVE_DATABASE_TYPES
-
end
-
-
# Returns the current database encoding format as a string, eg: 'UTF-8'
-
2
def encoding
-
@connection.encoding.to_s
-
end
-
-
2
def supports_explain?
-
true
-
end
-
-
# QUOTING ==================================================
-
-
2
def _quote(value) # :nodoc:
-
241
case value
-
when Type::Binary::Data
-
"x'#{value.hex}'"
-
else
-
241
super
-
end
-
end
-
-
2
def _type_cast(value) # :nodoc:
-
165
case value
-
when BigDecimal
-
1
value.to_f
-
when String
-
29
if value.encoding == Encoding::ASCII_8BIT
-
super(value.encode(Encoding::UTF_8))
-
else
-
29
super
-
end
-
else
-
135
super
-
end
-
end
-
-
2
def quote_string(s) #:nodoc:
-
73
@connection.class.quote(s)
-
end
-
-
2
def quote_table_name_for_assignment(table, attr)
-
quote_column_name(attr)
-
end
-
-
2
def quote_column_name(name) #:nodoc:
-
927
%Q("#{name.to_s.gsub('"', '""')}")
-
end
-
-
# Quote date/time values for use in SQL input. Includes microseconds
-
# if the value is a Time responding to usec.
-
2
def quoted_date(value) #:nodoc:
-
58
if value.respond_to?(:usec)
-
58
"#{super}.#{sprintf("%06d", value.usec)}"
-
else
-
super
-
end
-
end
-
-
#--
-
# DATABASE STATEMENTS ======================================
-
#++
-
-
2
def explain(arel, binds = [])
-
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
-
ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', []))
-
end
-
-
2
class ExplainPrettyPrinter
-
# Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
-
# the output of the SQLite shell:
-
#
-
# 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
-
# 0|1|1|SCAN TABLE posts (~100000 rows)
-
#
-
2
def pp(result) # :nodoc:
-
result.rows.map do |row|
-
row.join('|')
-
end.join("\n") + "\n"
-
end
-
end
-
-
2
def exec_query(sql, name = nil, binds = [])
-
222
type_casted_binds = binds.map { |col, val|
-
150
[col, type_cast(val, col)]
-
}
-
-
222
log(sql, name, type_casted_binds) do
-
# Don't cache statements if they are not prepared
-
222
if without_prepared_statement?(binds)
-
93
stmt = @connection.prepare(sql)
-
93
begin
-
93
cols = stmt.columns
-
93
records = stmt.to_a
-
ensure
-
93
stmt.close
-
end
-
93
stmt = records
-
else
-
129
cache = @statements[sql] ||= {
-
:stmt => @connection.prepare(sql)
-
}
-
129
stmt = cache[:stmt]
-
129
cols = cache[:cols] ||= stmt.columns
-
129
stmt.reset!
-
279
stmt.bind_params type_casted_binds.map { |_, val| val }
-
end
-
-
222
ActiveRecord::Result.new(cols, stmt.to_a)
-
end
-
end
-
-
2
def exec_delete(sql, name = 'SQL', binds = [])
-
23
exec_query(sql, name, binds)
-
23
@connection.changes
-
end
-
2
alias :exec_update :exec_delete
-
-
2
def last_inserted_id(result)
-
6
@connection.last_insert_row_id
-
end
-
-
2
def execute(sql, name = nil) #:nodoc:
-
172
log(sql, name) { @connection.execute(sql) }
-
end
-
-
2
def update_sql(sql, name = nil) #:nodoc:
-
super
-
@connection.changes
-
end
-
-
2
def delete_sql(sql, name = nil) #:nodoc:
-
sql += " WHERE 1=1" unless sql =~ /WHERE/i
-
super sql, name
-
end
-
-
2
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
-
super
-
id_value || @connection.last_insert_row_id
-
end
-
2
alias :create :insert_sql
-
-
2
def select_rows(sql, name = nil, binds = [])
-
exec_query(sql, name, binds).rows
-
end
-
-
2
def begin_db_transaction #:nodoc:
-
82
log('begin transaction',nil) { @connection.transaction }
-
end
-
-
2
def commit_db_transaction #:nodoc:
-
4
log('commit transaction',nil) { @connection.commit }
-
end
-
-
2
def exec_rollback_db_transaction #:nodoc:
-
78
log('rollback transaction',nil) { @connection.rollback }
-
end
-
-
# SCHEMA STATEMENTS ========================================
-
-
2
def tables(name = nil, table_name = nil) #:nodoc:
-
4
sql = <<-SQL
-
SELECT name
-
FROM sqlite_master
-
WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
-
SQL
-
4
sql << " AND name = #{quote_table_name(table_name)}" if table_name
-
-
4
exec_query(sql, 'SCHEMA').map do |row|
-
14
row['name']
-
end
-
end
-
2
alias data_sources tables
-
-
2
def table_exists?(table_name)
-
2
table_name && tables(nil, table_name).any?
-
end
-
2
alias data_source_exists? table_exists?
-
-
# Returns an array of +Column+ objects for the table specified by +table_name+.
-
2
def columns(table_name) #:nodoc:
-
22
table_structure(table_name).map do |field|
-
174
case field["dflt_value"]
-
when /^null$/i
-
field["dflt_value"] = nil
-
when /^'(.*)'$/m
-
8
field["dflt_value"] = $1.gsub("''", "'")
-
when /^"(.*)"$/m
-
field["dflt_value"] = $1.gsub('""', '"')
-
end
-
-
174
sql_type = field['type']
-
174
cast_type = lookup_cast_type(sql_type)
-
174
new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0)
-
end
-
end
-
-
# Returns an array of indexes for the given table.
-
2
def indexes(table_name, name = nil) #:nodoc:
-
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
-
sql = <<-SQL
-
SELECT sql
-
FROM sqlite_master
-
WHERE name=#{quote(row['name'])} AND type='index'
-
UNION ALL
-
SELECT sql
-
FROM sqlite_temp_master
-
WHERE name=#{quote(row['name'])} AND type='index'
-
SQL
-
index_sql = exec_query(sql).first['sql']
-
match = /\sWHERE\s+(.+)$/i.match(index_sql)
-
where = match[1] if match
-
IndexDefinition.new(
-
table_name,
-
row['name'],
-
row['unique'] != 0,
-
exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
-
col['name']
-
}, nil, nil, where)
-
end
-
end
-
-
2
def primary_key(table_name) #:nodoc:
-
96
pks = table_structure(table_name).select { |f| f['pk'] > 0 }
-
10
return nil unless pks.count == 1
-
10
pks[0]['name']
-
end
-
-
2
def remove_index!(table_name, index_name) #:nodoc:
-
exec_query "DROP INDEX #{quote_column_name(index_name)}"
-
end
-
-
# Renames a table.
-
#
-
# Example:
-
# rename_table('octopuses', 'octopi')
-
2
def rename_table(table_name, new_name)
-
exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
-
rename_table_indexes(table_name, new_name)
-
end
-
-
# See: http://www.sqlite.org/lang_altertable.html
-
# SQLite has an additional restriction on the ALTER TABLE statement
-
2
def valid_alter_table_type?(type)
-
type.to_sym != :primary_key
-
end
-
-
2
def add_column(table_name, column_name, type, options = {}) #:nodoc:
-
if valid_alter_table_type?(type)
-
super(table_name, column_name, type, options)
-
else
-
alter_table(table_name) do |definition|
-
definition.column(column_name, type, options)
-
end
-
end
-
end
-
-
2
def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
-
alter_table(table_name) do |definition|
-
definition.remove_column column_name
-
end
-
end
-
-
2
def change_column_default(table_name, column_name, default) #:nodoc:
-
alter_table(table_name) do |definition|
-
definition[column_name].default = default
-
end
-
end
-
-
2
def change_column_null(table_name, column_name, null, default = nil)
-
unless null || default.nil?
-
exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
-
end
-
alter_table(table_name) do |definition|
-
definition[column_name].null = null
-
end
-
end
-
-
2
def change_column(table_name, column_name, type, options = {}) #:nodoc:
-
alter_table(table_name) do |definition|
-
include_default = options_include_default?(options)
-
definition[column_name].instance_eval do
-
self.type = type
-
self.limit = options[:limit] if options.include?(:limit)
-
self.default = options[:default] if include_default
-
self.null = options[:null] if options.include?(:null)
-
self.precision = options[:precision] if options.include?(:precision)
-
self.scale = options[:scale] if options.include?(:scale)
-
end
-
end
-
end
-
-
2
def rename_column(table_name, column_name, new_column_name) #:nodoc:
-
column = column_for(table_name, column_name)
-
alter_table(table_name, rename: {column.name => new_column_name.to_s})
-
rename_column_indexes(table_name, column.name, new_column_name)
-
end
-
-
2
protected
-
-
2
def initialize_type_map(m)
-
2
super
-
2
m.register_type(/binary/i, SQLite3Binary.new)
-
end
-
-
2
def table_structure(table_name)
-
32
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash
-
32
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
-
32
structure
-
end
-
-
2
def alter_table(table_name, options = {}) #:nodoc:
-
altered_table_name = "a#{table_name}"
-
caller = lambda {|definition| yield definition if block_given?}
-
-
transaction do
-
move_table(table_name, altered_table_name,
-
options.merge(:temporary => true))
-
move_table(altered_table_name, table_name, &caller)
-
end
-
end
-
-
2
def move_table(from, to, options = {}, &block) #:nodoc:
-
copy_table(from, to, options, &block)
-
drop_table(from)
-
end
-
-
2
def copy_table(from, to, options = {}) #:nodoc:
-
from_primary_key = primary_key(from)
-
options[:id] = false
-
create_table(to, options) do |definition|
-
@definition = definition
-
@definition.primary_key(from_primary_key) if from_primary_key.present?
-
columns(from).each do |column|
-
column_name = options[:rename] ?
-
(options[:rename][column.name] ||
-
options[:rename][column.name.to_sym] ||
-
column.name) : column.name
-
next if column_name == from_primary_key
-
-
@definition.column(column_name, column.type,
-
:limit => column.limit, :default => column.default,
-
:precision => column.precision, :scale => column.scale,
-
:null => column.null)
-
end
-
yield @definition if block_given?
-
end
-
copy_table_indexes(from, to, options[:rename] || {})
-
copy_table_contents(from, to,
-
@definition.columns.map {|column| column.name},
-
options[:rename] || {})
-
end
-
-
2
def copy_table_indexes(from, to, rename = {}) #:nodoc:
-
indexes(from).each do |index|
-
name = index.name
-
if to == "a#{from}"
-
name = "t#{name}"
-
elsif from == "a#{to}"
-
name = name[1..-1]
-
end
-
-
to_column_names = columns(to).map { |c| c.name }
-
columns = index.columns.map {|c| rename[c] || c }.select do |column|
-
to_column_names.include?(column)
-
end
-
-
unless columns.empty?
-
# index name can't be the same
-
opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
-
opts[:unique] = true if index.unique
-
add_index(to, columns, opts)
-
end
-
end
-
end
-
-
2
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
-
column_mappings = Hash[columns.map {|name| [name, name]}]
-
rename.each { |a| column_mappings[a.last] = a.first }
-
from_columns = columns(from).collect {|col| col.name}
-
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
-
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
-
-
quoted_to = quote_table_name(to)
-
-
raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }]
-
-
exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
-
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
-
-
column_values = columns.map do |col|
-
quote(row[column_mappings[col]], raw_column_mappings[col])
-
end
-
-
sql << column_values * ', '
-
sql << ')'
-
exec_query sql
-
end
-
end
-
-
2
def sqlite_version
-
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
-
end
-
-
2
def translate_exception(exception, message)
-
case exception.message
-
# SQLite 3.8.2 returns a newly formatted error message:
-
# UNIQUE constraint failed: *table_name*.*column_name*
-
# Older versions of SQLite return:
-
# column *column_name* is not unique
-
when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
-
RecordNotUnique.new(message, exception)
-
else
-
super
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module ConnectionAdapters
-
2
class StatementPool
-
2
include Enumerable
-
-
2
def initialize(connection, max = 1000)
-
2
@connection = connection
-
2
@max = max
-
end
-
-
2
def each
-
raise NotImplementedError
-
end
-
-
2
def key?(key)
-
raise NotImplementedError
-
end
-
-
2
def [](key)
-
raise NotImplementedError
-
end
-
-
2
def length
-
raise NotImplementedError
-
end
-
-
2
def []=(sql, key)
-
raise NotImplementedError
-
end
-
-
2
def clear
-
raise NotImplementedError
-
end
-
-
2
def delete(key)
-
raise NotImplementedError
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module ConnectionHandling
-
8
RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] }
-
8
DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
-
-
# Establishes the connection to the database. Accepts a hash as input where
-
# the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
-
# example for regular databases (MySQL, Postgresql, etc):
-
#
-
# ActiveRecord::Base.establish_connection(
-
# adapter: "mysql",
-
# host: "localhost",
-
# username: "myuser",
-
# password: "mypass",
-
# database: "somedatabase"
-
# )
-
#
-
# Example for SQLite database:
-
#
-
# ActiveRecord::Base.establish_connection(
-
# adapter: "sqlite3",
-
# database: "path/to/dbfile"
-
# )
-
#
-
# Also accepts keys as strings (for parsing from YAML for example):
-
#
-
# ActiveRecord::Base.establish_connection(
-
# "adapter" => "sqlite3",
-
# "database" => "path/to/dbfile"
-
# )
-
#
-
# Or a URL:
-
#
-
# ActiveRecord::Base.establish_connection(
-
# "postgres://myuser:mypass@localhost/somedatabase"
-
# )
-
#
-
# In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails
-
# automatically loads the contents of config/database.yml into it),
-
# a symbol can also be given as argument, representing a key in the
-
# configuration hash:
-
#
-
# ActiveRecord::Base.establish_connection(:production)
-
#
-
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
-
# may be returned on an error.
-
2
def establish_connection(spec = nil)
-
2
spec ||= DEFAULT_ENV.call.to_sym
-
2
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
-
2
spec = resolver.spec(spec)
-
-
2
unless respond_to?(spec.adapter_method)
-
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
-
end
-
-
2
remove_connection
-
2
connection_handler.establish_connection self, spec
-
end
-
-
2
class MergeAndResolveDefaultUrlConfig # :nodoc:
-
2
def initialize(raw_configurations)
-
4
@raw_config = raw_configurations.dup
-
4
@env = DEFAULT_ENV.call.to_s
-
end
-
-
# Returns fully resolved connection hashes.
-
# Merges connection information from `ENV['DATABASE_URL']` if available.
-
2
def resolve
-
4
ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
-
end
-
-
2
private
-
2
def config
-
4
@raw_config.dup.tap do |cfg|
-
4
if url = ENV['DATABASE_URL']
-
cfg[@env] ||= {}
-
cfg[@env]["url"] ||= url
-
end
-
end
-
end
-
end
-
-
# Returns the connection currently associated with the class. This can
-
# also be used to "borrow" the connection to do database work unrelated
-
# to any of the specific Active Records.
-
2
def connection
-
646
retrieve_connection
-
end
-
-
2
def connection_id
-
763
ActiveRecord::RuntimeRegistry.connection_id
-
end
-
-
2
def connection_id=(connection_id)
-
2
ActiveRecord::RuntimeRegistry.connection_id = connection_id
-
end
-
-
# Returns the configuration of the associated connection as a hash:
-
#
-
# ActiveRecord::Base.connection_config
-
# # => {pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}
-
#
-
# Please use only for reading.
-
2
def connection_config
-
connection_pool.spec.config
-
end
-
-
2
def connection_pool
-
connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
-
end
-
-
2
def retrieve_connection
-
646
connection_handler.retrieve_connection(self)
-
end
-
-
# Returns +true+ if Active Record is connected.
-
2
def connected?
-
47
connection_handler.connected?(self)
-
end
-
-
2
def remove_connection(klass = self)
-
2
connection_handler.remove_connection(klass)
-
end
-
-
2
def clear_cache! # :nodoc:
-
connection.schema_cache.clear!
-
end
-
-
2
delegate :clear_active_connections!, :clear_reloadable_connections!,
-
:clear_all_connections!, :to => :connection_handler
-
end
-
end
-
2
require 'thread'
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
2
require 'active_support/core_ext/object/duplicable'
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
2
module Core
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
##
-
# :singleton-method:
-
#
-
# Accepts a logger conforming to the interface of Log4r which is then
-
# passed on to any new database connections made and which can be
-
# retrieved on both a class and instance level by calling +logger+.
-
2
mattr_accessor :logger, instance_writer: false
-
-
##
-
# Contains the database configuration - as is typically stored in config/database.yml -
-
# as a Hash.
-
#
-
# For example, the following database.yml...
-
#
-
# development:
-
# adapter: sqlite3
-
# database: db/development.sqlite3
-
#
-
# production:
-
# adapter: sqlite3
-
# database: db/production.sqlite3
-
#
-
# ...would result in ActiveRecord::Base.configurations to look like this:
-
#
-
# {
-
# 'development' => {
-
# 'adapter' => 'sqlite3',
-
# 'database' => 'db/development.sqlite3'
-
# },
-
# 'production' => {
-
# 'adapter' => 'sqlite3',
-
# 'database' => 'db/production.sqlite3'
-
# }
-
# }
-
2
def self.configurations=(config)
-
4
@@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
-
end
-
2
self.configurations = {}
-
-
# Returns fully resolved configurations hash
-
2
def self.configurations
-
2
@@configurations
-
end
-
-
##
-
# :singleton-method:
-
# Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
-
# dates and times from the database. This is set to :utc by default.
-
2
mattr_accessor :default_timezone, instance_writer: false
-
2
self.default_timezone = :utc
-
-
##
-
# :singleton-method:
-
# Specifies the format to use when dumping the database schema with Rails'
-
# Rakefile. If :sql, the schema is dumped as (potentially database-
-
# specific) SQL statements. If :ruby, the schema is dumped as an
-
# ActiveRecord::Schema file which can be loaded into any database that
-
# supports migrations. Use :ruby if you want to have different database
-
# adapters for, e.g., your development and test environments.
-
2
mattr_accessor :schema_format, instance_writer: false
-
2
self.schema_format = :ruby
-
-
##
-
# :singleton-method:
-
# Specify whether or not to use timestamps for migration versions
-
2
mattr_accessor :timestamped_migrations, instance_writer: false
-
2
self.timestamped_migrations = true
-
-
##
-
# :singleton-method:
-
# Specify whether schema dump should happen at the end of the
-
# db:migrate rake task. This is true by default, which is useful for the
-
# development environment. This should ideally be false in the production
-
# environment where dumping schema is rarely needed.
-
2
mattr_accessor :dump_schema_after_migration, instance_writer: false
-
2
self.dump_schema_after_migration = true
-
-
2
mattr_accessor :maintain_test_schema, instance_accessor: false
-
-
2
def self.disable_implicit_join_references=(value)
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
Implicit join references were removed with Rails 4.1.
-
Make sure to remove this configuration because it does nothing.
-
MSG
-
end
-
-
2
class_attribute :default_connection_handler, instance_writer: false
-
2
class_attribute :find_by_statement_cache
-
-
2
def self.connection_handler
-
787
ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
-
end
-
-
2
def self.connection_handler=(handler)
-
ActiveRecord::RuntimeRegistry.connection_handler = handler
-
end
-
-
2
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
-
end
-
-
2
module ClassMethods
-
2
def allocate
-
162
define_attribute_methods
-
162
super
-
end
-
-
2
def initialize_find_by_cache # :nodoc:
-
12
self.find_by_statement_cache = {}.extend(Mutex_m)
-
end
-
-
2
def inherited(child_class) # :nodoc:
-
12
child_class.initialize_find_by_cache
-
12
super
-
end
-
-
2
def find(*ids) # :nodoc:
-
# We don't have cache keys for this stuff yet
-
77
return super unless ids.length == 1
-
# Allow symbols to super to maintain compatibility for deprecated finders until Rails 5
-
77
return super if ids.first.kind_of?(Symbol)
-
return super if block_given? ||
-
77
primary_key.nil? ||
-
default_scopes.any? ||
-
current_scope ||
-
columns_hash.include?(inheritance_column) ||
-
ids.first.kind_of?(Array)
-
-
15
id = ids.first
-
15
if ActiveRecord::Base === id
-
id = id.id
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
You are passing an instance of ActiveRecord::Base to `find`.
-
Please pass the id of the object by calling `.id`
-
MSG
-
end
-
15
key = primary_key
-
-
15
s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
-
3
find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
-
3
where(key => params.bind).limit(1)
-
}
-
}
-
15
record = s.execute([id], self, connection).first
-
15
unless record
-
raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
-
end
-
15
record
-
rescue RangeError
-
raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'"
-
end
-
-
2
def find_by(*args) # :nodoc:
-
4
return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
-
4
return super if default_scopes.any?
-
-
4
hash = args.first
-
-
4
return super if hash.values.any? { |v|
-
4
v.nil? || Array === v || Hash === v
-
}
-
-
# We can't cache Post.find_by(author: david) ...yet
-
8
return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
-
-
4
key = hash.keys
-
-
4
klass = self
-
4
s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
-
1
find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
-
1
wheres = key.each_with_object({}) { |param,o|
-
1
o[param] = params.bind
-
}
-
1
klass.where(wheres).limit(1)
-
}
-
}
-
4
begin
-
4
s.execute(hash.values, self, connection).first
-
rescue TypeError => e
-
raise ActiveRecord::StatementInvalid.new(e.message, e)
-
rescue RangeError
-
nil
-
end
-
end
-
-
2
def find_by!(*args) # :nodoc:
-
find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}")
-
end
-
-
2
def initialize_generated_modules # :nodoc:
-
14
generated_association_methods
-
end
-
-
2
def generated_association_methods
-
@generated_association_methods ||= begin
-
14
mod = const_set(:GeneratedAssociationMethods, Module.new)
-
14
include mod
-
14
mod
-
54
end
-
end
-
-
# Returns a string like 'Post(id:integer, title:string, body:text)'
-
2
def inspect
-
if self == Base
-
super
-
elsif abstract_class?
-
"#{super}(abstract)"
-
elsif !connected?
-
"#{super} (call '#{super}.connection' to establish a connection)"
-
elsif table_exists?
-
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
-
"#{super}(#{attr_list})"
-
else
-
"#{super}(Table doesn't exist)"
-
end
-
end
-
-
# Overwrite the default class equality method to provide support for association proxies.
-
2
def ===(object)
-
290
object.is_a?(self)
-
end
-
-
# Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
-
#
-
# class Post < ActiveRecord::Base
-
# scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) }
-
# end
-
2
def arel_table # :nodoc:
-
458
@arel_table ||= Arel::Table.new(table_name, arel_engine)
-
end
-
-
# Returns the Arel engine.
-
2
def arel_engine # :nodoc:
-
@arel_engine ||=
-
if Base == self || connection_handler.retrieve_connection_pool(self)
-
12
self
-
else
-
superclass.arel_engine
-
12
end
-
end
-
-
2
private
-
-
2
def relation #:nodoc:
-
238
relation = Relation.create(self, arel_table)
-
-
238
if finder_needs_type_condition?
-
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
-
else
-
238
relation
-
end
-
end
-
end
-
-
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
-
# attributes but not yet saved (pass a hash with key names matching the associated table column names).
-
# In both instances, valid attribute keys are determined by the column names of the associated table --
-
# hence you can't have attributes that aren't part of the table columns.
-
#
-
# ==== Example:
-
# # Instantiates a single new object
-
# User.new(first_name: 'Jamie')
-
2
def initialize(attributes = nil, options = {})
-
21
@attributes = self.class._default_attributes.dup
-
21
self.class.define_attribute_methods
-
-
21
init_internals
-
21
initialize_internals_callback
-
-
# +options+ argument is only needed to make protected_attributes gem easier to hook.
-
# Remove it when we drop support to this gem.
-
21
init_attributes(attributes, options) if attributes
-
-
21
yield self if block_given?
-
21
_run_initialize_callbacks
-
end
-
-
# Initialize an empty model object from +coder+. +coder+ should be
-
# the result of previously encoding an Active Record model, using
-
# `encode_with`
-
#
-
# class Post < ActiveRecord::Base
-
# end
-
#
-
# old_post = Post.new(title: "hello world")
-
# coder = {}
-
# old_post.encode_with(coder)
-
#
-
# post = Post.allocate
-
# post.init_with(coder)
-
# post.title # => 'hello world'
-
2
def init_with(coder)
-
162
coder = LegacyYamlAdapter.convert(self.class, coder)
-
162
@attributes = coder['attributes']
-
-
162
init_internals
-
-
162
@new_record = coder['new_record']
-
-
162
self.class.define_attribute_methods
-
-
162
_run_find_callbacks
-
162
_run_initialize_callbacks
-
-
162
self
-
end
-
-
##
-
# :method: clone
-
# Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied.
-
# That means that modifying attributes of the clone will modify the original, since they will both point to the
-
# same attributes hash. If you need a copy of your attributes hash, please use the #dup method.
-
#
-
# user = User.first
-
# new_user = user.clone
-
# user.name # => "Bob"
-
# new_user.name = "Joe"
-
# user.name # => "Joe"
-
#
-
# user.object_id == new_user.object_id # => false
-
# user.name.object_id == new_user.name.object_id # => true
-
#
-
# user.name.object_id == user.dup.name.object_id # => false
-
-
##
-
# :method: dup
-
# Duped objects have no id assigned and are treated as new records. Note
-
# that this is a "shallow" copy as it copies the object's attributes
-
# only, not its associations. The extent of a "deep" copy is application
-
# specific and is therefore left to the application to implement according
-
# to its need.
-
# The dup method does not preserve the timestamps (created|updated)_(at|on).
-
-
##
-
2
def initialize_dup(other) # :nodoc:
-
@attributes = @attributes.dup
-
@attributes.reset(self.class.primary_key)
-
-
_run_initialize_callbacks
-
-
@aggregation_cache = {}
-
@association_cache = {}
-
-
@new_record = true
-
@destroyed = false
-
-
super
-
end
-
-
# Populate +coder+ with attributes about this record that should be
-
# serialized. The structure of +coder+ defined in this method is
-
# guaranteed to match the structure of +coder+ passed to the +init_with+
-
# method.
-
#
-
# Example:
-
#
-
# class Post < ActiveRecord::Base
-
# end
-
# coder = {}
-
# Post.new.encode_with(coder)
-
# coder # => {"attributes" => {"id" => nil, ... }}
-
2
def encode_with(coder)
-
# FIXME: Remove this when we better serialize attributes
-
coder['raw_attributes'] = attributes_before_type_cast
-
coder['attributes'] = @attributes
-
coder['new_record'] = new_record?
-
coder['active_record_yaml_version'] = 0
-
end
-
-
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
-
# is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
-
#
-
# Note that new records are different from any other record by definition, unless the
-
# other record is the receiver itself. Besides, if you fetch existing records with
-
# +select+ and leave the ID out, you're on your own, this predicate will return false.
-
#
-
# Note also that destroying a record preserves its ID in the model instance, so deleted
-
# models are still comparable.
-
2
def ==(comparison_object)
-
super ||
-
comparison_object.instance_of?(self.class) &&
-
!id.nil? &&
-
29
comparison_object.id == id
-
end
-
2
alias :eql? :==
-
-
# Delegates to id in order to allow two records of the same type and id to work with something like:
-
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
-
2
def hash
-
if id
-
id.hash
-
else
-
super
-
end
-
end
-
-
# Clone and freeze the attributes hash such that associations are still
-
# accessible, even on destroyed records, but cloned models will not be
-
# frozen.
-
2
def freeze
-
3
@attributes = @attributes.clone.freeze
-
3
self
-
end
-
-
# Returns +true+ if the attributes hash has been frozen.
-
2
def frozen?
-
41
@attributes.frozen?
-
end
-
-
# Allows sort on objects
-
2
def <=>(other_object)
-
if other_object.is_a?(self.class)
-
self.to_key <=> other_object.to_key
-
else
-
super
-
end
-
end
-
-
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
-
# attributes will be marked as read only since they cannot be saved.
-
2
def readonly?
-
13
@readonly
-
end
-
-
# Marks this record as read only.
-
2
def readonly!
-
@readonly = true
-
end
-
-
2
def connection_handler
-
self.class.connection_handler
-
end
-
-
# Returns the contents of the record as a nicely formatted string.
-
2
def inspect
-
# We check defined?(@attributes) not to issue warnings if the object is
-
# allocated but not initialized.
-
inspection = if defined?(@attributes) && @attributes
-
self.class.column_names.collect { |name|
-
if has_attribute?(name)
-
"#{name}: #{attribute_for_inspect(name)}"
-
end
-
}.compact.join(", ")
-
else
-
"not initialized"
-
end
-
"#<#{self.class} #{inspection}>"
-
end
-
-
# Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record`
-
# when pp is required.
-
2
def pretty_print(pp)
-
return super if custom_inspect_method_defined?
-
pp.object_address_group(self) do
-
if defined?(@attributes) && @attributes
-
column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
-
pp.seplist(column_names, proc { pp.text ',' }) do |column_name|
-
column_value = read_attribute(column_name)
-
pp.breakable ' '
-
pp.group(1) do
-
pp.text column_name
-
pp.text ':'
-
pp.breakable
-
pp.pp column_value
-
end
-
end
-
else
-
pp.breakable ' '
-
pp.text 'not initialized'
-
end
-
end
-
end
-
-
# Returns a hash of the given methods with their names as keys and returned values as values.
-
2
def slice(*methods)
-
Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
-
end
-
-
2
private
-
-
2
def set_transaction_state(state) # :nodoc:
-
23
@transaction_state = state
-
end
-
-
2
def has_transactional_callbacks? # :nodoc:
-
55
!_rollback_callbacks.empty? || !_commit_callbacks.empty?
-
end
-
-
# Updates the attributes on this particular ActiveRecord object so that
-
# if it is associated with a transaction, then the state of the AR object
-
# will be updated to reflect the current state of the transaction
-
#
-
# The @transaction_state variable stores the states of the associated
-
# transaction. This relies on the fact that a transaction can only be in
-
# one rollback or commit (otherwise a list of states would be required)
-
# Each AR object inside of a transaction carries that transaction's
-
# TransactionState.
-
#
-
# This method checks to see if the ActiveRecord object's state reflects
-
# the TransactionState, and rolls back or commits the ActiveRecord object
-
# as appropriate.
-
#
-
# Since ActiveRecord objects can be inside multiple transactions, this
-
# method recursively goes through the parent of the TransactionState and
-
# checks if the ActiveRecord object reflects the state of the object.
-
2
def sync_with_transaction_state
-
217
update_attributes_from_transaction_state(@transaction_state, 0)
-
end
-
-
2
def update_attributes_from_transaction_state(transaction_state, depth)
-
217
@reflects_state = [false] if depth == 0
-
-
217
if transaction_state && transaction_state.finalized? && !has_transactional_callbacks?
-
32
unless @reflects_state[depth]
-
32
restore_transaction_record_state if transaction_state.rolledback?
-
32
clear_transaction_record_state
-
32
@reflects_state[depth] = true
-
end
-
-
32
if transaction_state.parent && !@reflects_state[depth+1]
-
update_attributes_from_transaction_state(transaction_state.parent, depth+1)
-
end
-
end
-
end
-
-
# Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
-
# of the array, and then rescues from the possible NoMethodError. If those elements are
-
# ActiveRecord::Base's, then this triggers the various method_missing's that we have,
-
# which significantly impacts upon performance.
-
#
-
# So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
-
#
-
# See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html
-
2
def to_ary # :nodoc:
-
nil
-
end
-
-
2
def init_internals
-
183
@aggregation_cache = {}
-
183
@association_cache = {}
-
183
@readonly = false
-
183
@destroyed = false
-
183
@marked_for_destruction = false
-
183
@destroyed_by_association = nil
-
183
@new_record = true
-
183
@txn = nil
-
183
@_start_transaction_state = {}
-
183
@transaction_state = nil
-
end
-
-
2
def initialize_internals_callback
-
end
-
-
# This method is needed to make protected_attributes gem easier to hook.
-
# Remove it when we drop support to this gem.
-
2
def init_attributes(attributes, options)
-
12
assign_attributes(attributes)
-
end
-
-
2
def thaw
-
4
if frozen?
-
@attributes = @attributes.dup
-
end
-
end
-
-
2
def custom_inspect_method_defined?
-
self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record Counter Cache
-
2
module CounterCache
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
# Resets one or more counter caches to their correct value using an SQL
-
# count query. This is useful when adding new counter caches, or if the
-
# counter has been corrupted or modified directly by SQL.
-
#
-
# ==== Parameters
-
#
-
# * +id+ - The id of the object you wish to reset a counter on.
-
# * +counters+ - One or more association counters to reset. Association name or counter name can be given.
-
#
-
# ==== Examples
-
#
-
# # For Post with id #1 records reset the comments_count
-
# Post.reset_counters(1, :comments)
-
2
def reset_counters(id, *counters)
-
object = find(id)
-
counters.each do |counter_association|
-
has_many_association = _reflect_on_association(counter_association)
-
unless has_many_association
-
has_many = reflect_on_all_associations(:has_many)
-
has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
-
counter_association = has_many_association.plural_name if has_many_association
-
end
-
raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association
-
-
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
-
has_many_association = has_many_association.through_reflection
-
end
-
-
foreign_key = has_many_association.foreign_key.to_s
-
child_class = has_many_association.klass
-
reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
-
counter_name = reflection.counter_cache_column
-
-
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
-
arel_table[counter_name] => object.send(counter_association).count(:all)
-
}, primary_key)
-
connection.update stmt
-
end
-
return true
-
end
-
-
# A generic "counter updater" implementation, intended primarily to be
-
# used by increment_counter and decrement_counter, but which may also
-
# be useful on its own. It simply does a direct SQL update for the record
-
# with the given ID, altering the given hash of counters by the amount
-
# given by the corresponding value:
-
#
-
# ==== Parameters
-
#
-
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
-
# * +counters+ - A Hash containing the names of the fields
-
# to update as keys and the amount to update the field by as values.
-
#
-
# ==== Examples
-
#
-
# # For the Post with id of 5, decrement the comment_count by 1, and
-
# # increment the action_count by 1
-
# Post.update_counters 5, comment_count: -1, action_count: 1
-
# # Executes the following SQL:
-
# # UPDATE posts
-
# # SET comment_count = COALESCE(comment_count, 0) - 1,
-
# # action_count = COALESCE(action_count, 0) + 1
-
# # WHERE id = 5
-
#
-
# # For the Posts with id of 10 and 15, increment the comment_count by 1
-
# Post.update_counters [10, 15], comment_count: 1
-
# # Executes the following SQL:
-
# # UPDATE posts
-
# # SET comment_count = COALESCE(comment_count, 0) + 1
-
# # WHERE id IN (10, 15)
-
2
def update_counters(id, counters)
-
updates = counters.map do |counter_name, value|
-
operator = value < 0 ? '-' : '+'
-
quoted_column = connection.quote_column_name(counter_name)
-
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
-
end
-
-
unscoped.where(primary_key => id).update_all updates.join(', ')
-
end
-
-
# Increment a numeric field by one, via a direct SQL update.
-
#
-
# This method is used primarily for maintaining counter_cache columns that are
-
# used to store aggregate values. For example, a DiscussionBoard may cache
-
# posts_count and comments_count to avoid running an SQL query to calculate the
-
# number of posts and comments there are, each time it is displayed.
-
#
-
# ==== Parameters
-
#
-
# * +counter_name+ - The name of the field that should be incremented.
-
# * +id+ - The id of the object that should be incremented or an Array of ids.
-
#
-
# ==== Examples
-
#
-
# # Increment the post_count column for the record with an id of 5
-
# DiscussionBoard.increment_counter(:post_count, 5)
-
2
def increment_counter(counter_name, id)
-
update_counters(id, counter_name => 1)
-
end
-
-
# Decrement a numeric field by one, via a direct SQL update.
-
#
-
# This works the same as increment_counter but reduces the column value by
-
# 1 instead of increasing it.
-
#
-
# ==== Parameters
-
#
-
# * +counter_name+ - The name of the field that should be decremented.
-
# * +id+ - The id of the object that should be decremented or an Array of ids.
-
#
-
# ==== Examples
-
#
-
# # Decrement the post_count column for the record with an id of 5
-
# DiscussionBoard.decrement_counter(:post_count, 5)
-
2
def decrement_counter(counter_name, id)
-
update_counters(id, counter_name => -1)
-
end
-
end
-
-
2
protected
-
-
2
def actually_destroyed?
-
@_actually_destroyed
-
end
-
-
2
def clear_destroy_state
-
@_actually_destroyed = nil
-
end
-
-
2
private
-
-
2
def _create_record(*)
-
6
id = super
-
-
6
each_counter_cached_associations do |association|
-
if send(association.reflection.name)
-
association.increment_counters
-
@_after_create_counter_called = true
-
end
-
end
-
-
6
id
-
end
-
-
2
def destroy_row
-
3
affected_rows = super
-
-
3
if affected_rows > 0
-
3
each_counter_cached_associations do |association|
-
foreign_key = association.reflection.foreign_key.to_sym
-
unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
-
if send(association.reflection.name)
-
association.decrement_counters
-
end
-
end
-
end
-
end
-
-
3
affected_rows
-
end
-
-
2
def each_counter_cached_associations
-
9
_reflections.each do |name, reflection|
-
18
yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
-
end
-
end
-
-
end
-
end
-
2
module ActiveRecord
-
2
module DynamicMatchers #:nodoc:
-
# This code in this file seems to have a lot of indirection, but the indirection
-
# is there to provide extension points for the activerecord-deprecated_finders
-
# gem. When we stop supporting activerecord-deprecated_finders (from Rails 5),
-
# then we can remove the indirection.
-
-
2
def respond_to?(name, include_private = false)
-
777
if self == Base
-
16
super
-
else
-
761
match = Method.match(self, name)
-
761
match && match.valid? || super
-
end
-
end
-
-
2
private
-
-
2
def method_missing(name, *arguments, &block)
-
1
match = Method.match(self, name)
-
-
1
if match && match.valid?
-
1
match.define
-
1
send(name, *arguments, &block)
-
else
-
super
-
end
-
end
-
-
2
class Method
-
2
@matchers = []
-
-
2
class << self
-
2
attr_reader :matchers
-
-
2
def match(model, name)
-
2285
klass = matchers.find { |k| name =~ k.pattern }
-
762
klass.new(model, name) if klass
-
end
-
-
2
def pattern
-
1524
@pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
-
end
-
-
2
def prefix
-
raise NotImplementedError
-
end
-
-
2
def suffix
-
2
''
-
end
-
end
-
-
2
attr_reader :model, :name, :attribute_names
-
-
2
def initialize(model, name)
-
1
@model = model
-
1
@name = name.to_s
-
1
@attribute_names = @name.match(self.class.pattern)[1].split('_and_')
-
2
@attribute_names.map! { |n| @model.attribute_aliases[n] || n }
-
end
-
-
2
def valid?
-
2
attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
-
end
-
-
2
def define
-
1
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def self.#{name}(#{signature})
-
#{body}
-
end
-
CODE
-
end
-
-
2
def body
-
raise NotImplementedError
-
end
-
end
-
-
2
module Finder
-
# Extended in activerecord-deprecated_finders
-
2
def body
-
1
result
-
end
-
-
# Extended in activerecord-deprecated_finders
-
2
def result
-
1
"#{finder}(#{attributes_hash})"
-
end
-
-
# The parameters in the signature may have reserved Ruby words, in order
-
# to prevent errors, we start each param name with `_`.
-
#
-
# Extended in activerecord-deprecated_finders
-
2
def signature
-
2
attribute_names.map { |name| "_#{name}" }.join(', ')
-
end
-
-
# Given that the parameters starts with `_`, the finder needs to use the
-
# same parameter name.
-
2
def attributes_hash
-
2
"{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
-
end
-
-
2
def finder
-
raise NotImplementedError
-
end
-
end
-
-
2
class FindBy < Method
-
2
Method.matchers << self
-
2
include Finder
-
-
2
def self.prefix
-
2
"find_by"
-
end
-
-
2
def finder
-
1
"find_by"
-
end
-
end
-
-
2
class FindByBang < Method
-
2
Method.matchers << self
-
2
include Finder
-
-
2
def self.prefix
-
2
"find_by"
-
end
-
-
2
def self.suffix
-
2
"!"
-
end
-
-
2
def finder
-
"find_by!"
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/object/deep_dup'
-
-
2
module ActiveRecord
-
# Declare an enum attribute where the values map to integers in the database,
-
# but can be queried by name. Example:
-
#
-
# class Conversation < ActiveRecord::Base
-
# enum status: [ :active, :archived ]
-
# end
-
#
-
# # conversation.update! status: 0
-
# conversation.active!
-
# conversation.active? # => true
-
# conversation.status # => "active"
-
#
-
# # conversation.update! status: 1
-
# conversation.archived!
-
# conversation.archived? # => true
-
# conversation.status # => "archived"
-
#
-
# # conversation.status = 1
-
# conversation.status = "archived"
-
#
-
# conversation.status = nil
-
# conversation.status.nil? # => true
-
# conversation.status # => nil
-
#
-
# Scopes based on the allowed values of the enum field will be provided
-
# as well. With the above example:
-
#
-
# Conversation.active
-
# Conversation.archived
-
#
-
# You can set the default value from the database declaration, like:
-
#
-
# create_table :conversations do |t|
-
# t.column :status, :integer, default: 0
-
# end
-
#
-
# Good practice is to let the first declared status be the default.
-
#
-
# Finally, it's also possible to explicitly map the relation between attribute and
-
# database integer with a +Hash+:
-
#
-
# class Conversation < ActiveRecord::Base
-
# enum status: { active: 0, archived: 1 }
-
# end
-
#
-
# Note that when an +Array+ is used, the implicit mapping from the values to database
-
# integers is derived from the order the values appear in the array. In the example,
-
# <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
-
# is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
-
# database.
-
#
-
# Therefore, once a value is added to the enum array, its position in the array must
-
# be maintained, and new values should only be added to the end of the array. To
-
# remove unused values, the explicit +Hash+ syntax should be used.
-
#
-
# In rare circumstances you might need to access the mapping directly.
-
# The mappings are exposed through a class method with the pluralized attribute
-
# name:
-
#
-
# Conversation.statuses # => { "active" => 0, "archived" => 1 }
-
#
-
# Use that class method when you need to know the ordinal value of an enum:
-
#
-
# Conversation.where("status <> ?", Conversation.statuses[:archived])
-
#
-
# Where conditions on an enum attribute must use the ordinal value of an enum.
-
2
module Enum
-
2
def self.extended(base) # :nodoc:
-
2
base.class_attribute(:defined_enums, instance_writer: false)
-
2
base.defined_enums = {}
-
end
-
-
2
def inherited(base) # :nodoc:
-
12
base.defined_enums = defined_enums.deep_dup
-
12
super
-
end
-
-
2
def enum(definitions)
-
klass = self
-
definitions.each do |name, values|
-
# statuses = { }
-
enum_values = ActiveSupport::HashWithIndifferentAccess.new
-
name = name.to_sym
-
-
# def self.statuses statuses end
-
detect_enum_conflict!(name, name.to_s.pluralize, true)
-
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
-
-
_enum_methods_module.module_eval do
-
# def status=(value) self[:status] = statuses[value] end
-
klass.send(:detect_enum_conflict!, name, "#{name}=")
-
define_method("#{name}=") { |value|
-
if enum_values.has_key?(value) || value.blank?
-
self[name] = enum_values[value]
-
elsif enum_values.has_value?(value)
-
# Assigning a value directly is not a end-user feature, hence it's not documented.
-
# This is used internally to make building objects from the generated scopes work
-
# as expected, i.e. +Conversation.archived.build.archived?+ should be true.
-
self[name] = value
-
else
-
raise ArgumentError, "'#{value}' is not a valid #{name}"
-
end
-
}
-
-
# def status() statuses.key self[:status] end
-
klass.send(:detect_enum_conflict!, name, name)
-
define_method(name) { enum_values.key self[name] }
-
-
# def status_before_type_cast() statuses.key self[:status] end
-
klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
-
define_method("#{name}_before_type_cast") { enum_values.key self[name] }
-
-
pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
-
pairs.each do |value, i|
-
enum_values[value] = i
-
-
# def active?() status == 0 end
-
klass.send(:detect_enum_conflict!, name, "#{value}?")
-
define_method("#{value}?") { self[name] == i }
-
-
# def active!() update! status: :active end
-
klass.send(:detect_enum_conflict!, name, "#{value}!")
-
define_method("#{value}!") { update! name => value }
-
-
# scope :active, -> { where status: 0 }
-
klass.send(:detect_enum_conflict!, name, value, true)
-
klass.scope value, -> { klass.where name => i }
-
end
-
end
-
defined_enums[name.to_s] = enum_values
-
end
-
end
-
-
2
private
-
2
def _enum_methods_module
-
@_enum_methods_module ||= begin
-
mod = Module.new do
-
private
-
def save_changed_attribute(attr_name, old)
-
if (mapping = self.class.defined_enums[attr_name.to_s])
-
value = _read_attribute(attr_name)
-
if attribute_changed?(attr_name)
-
if mapping[old] == value
-
clear_attribute_changes([attr_name])
-
end
-
else
-
if old != value
-
set_attribute_was(attr_name, mapping.key(old))
-
end
-
end
-
else
-
super
-
end
-
end
-
end
-
include mod
-
mod
-
end
-
end
-
-
2
ENUM_CONFLICT_MESSAGE = \
-
"You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
-
"this will generate a %{type} method \"%{method}\", which is already defined " \
-
"by %{source}."
-
-
2
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
-
if klass_method && dangerous_class_method?(method_name)
-
raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
-
enum: enum_name,
-
klass: self.name,
-
type: 'class',
-
method: method_name,
-
source: 'Active Record'
-
}
-
elsif !klass_method && dangerous_attribute_method?(method_name)
-
raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
-
enum: enum_name,
-
klass: self.name,
-
type: 'instance',
-
method: method_name,
-
source: 'Active Record'
-
}
-
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
-
raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
-
enum: enum_name,
-
klass: self.name,
-
type: 'instance',
-
method: method_name,
-
source: 'another enum'
-
}
-
end
-
end
-
end
-
end
-
2
require 'active_support/lazy_load_hooks'
-
2
require 'active_record/explain_registry'
-
-
2
module ActiveRecord
-
2
module Explain
-
# Executes the block with the collect flag enabled. Queries are collected
-
# asynchronously by the subscriber and returned.
-
2
def collecting_queries_for_explain # :nodoc:
-
ExplainRegistry.collect = true
-
yield
-
ExplainRegistry.queries
-
ensure
-
ExplainRegistry.reset
-
end
-
-
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
-
# Returns a formatted string ready to be logged.
-
2
def exec_explain(queries) # :nodoc:
-
str = queries.map do |sql, bind|
-
[].tap do |msg|
-
msg << "EXPLAIN for: #{sql}"
-
unless bind.empty?
-
bind_msg = bind.map {|col, val| [col.name, val]}.inspect
-
msg.last << " #{bind_msg}"
-
end
-
msg << connection.explain(sql, bind)
-
end.join("\n")
-
end.join("\n")
-
-
# Overriding inspect to be more human readable, especially in the console.
-
def str.inspect
-
self
-
end
-
-
str
-
end
-
end
-
end
-
2
require 'active_support/per_thread_registry'
-
-
2
module ActiveRecord
-
# This is a thread locals registry for EXPLAIN. For example
-
#
-
# ActiveRecord::ExplainRegistry.queries
-
#
-
# returns the collected queries local to the current thread.
-
#
-
# See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
-
# for further details.
-
2
class ExplainRegistry # :nodoc:
-
2
extend ActiveSupport::PerThreadRegistry
-
-
2
attr_accessor :queries, :collect
-
-
2
def initialize
-
2
reset
-
end
-
-
2
def collect?
-
390
@collect
-
end
-
-
2
def reset
-
2
@collect = false
-
2
@queries = []
-
end
-
end
-
end
-
2
require 'active_support/notifications'
-
2
require 'active_record/explain_registry'
-
-
2
module ActiveRecord
-
2
class ExplainSubscriber # :nodoc:
-
2
def start(name, id, payload)
-
# unused
-
end
-
-
2
def finish(name, id, payload)
-
390
if ExplainRegistry.collect? && !ignore_payload?(payload)
-
ExplainRegistry.queries << payload.values_at(:sql, :binds)
-
end
-
end
-
-
# SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
-
# our own EXPLAINs now matter how loopingly beautiful that would be.
-
#
-
# On the other hand, we want to monitor the performance of our real database
-
# queries, not the performance of the access to the query cache.
-
2
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
-
2
EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i
-
2
def ignore_payload?(payload)
-
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
-
end
-
-
2
ActiveSupport::Notifications.subscribe("sql.active_record", new)
-
end
-
end
-
2
require 'erb'
-
2
require 'yaml'
-
-
2
module ActiveRecord
-
2
class FixtureSet
-
2
class File # :nodoc:
-
2
include Enumerable
-
-
##
-
# Open a fixture file named +file+. When called with a block, the block
-
# is called with the filehandle and the filehandle is automatically closed
-
# when the block finishes.
-
2
def self.open(file)
-
10
x = new file
-
10
block_given? ? yield(x) : x
-
end
-
-
2
def initialize(file)
-
10
@file = file
-
10
@rows = nil
-
end
-
-
2
def each(&block)
-
10
rows.each(&block)
-
end
-
-
-
2
private
-
2
def rows
-
10
return @rows if @rows
-
-
10
begin
-
10
data = YAML.load(render(IO.read(@file)))
-
rescue ArgumentError, Psych::SyntaxError => error
-
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
-
end
-
10
@rows = data ? validate(data).to_a : []
-
end
-
-
2
def render(content)
-
10
context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
-
10
ERB.new(content).result(context.get_binding)
-
end
-
-
# Validate our unmarshalled data.
-
2
def validate(data)
-
10
unless Hash === data || YAML::Omap === data
-
raise Fixture::FormatError, 'fixture is not a hash'
-
end
-
-
38
raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
-
10
data
-
end
-
end
-
end
-
end
-
2
require 'erb'
-
2
require 'yaml'
-
2
require 'zlib'
-
2
require 'active_support/dependencies'
-
2
require 'active_support/core_ext/digest/uuid'
-
2
require 'active_record/fixture_set/file'
-
2
require 'active_record/errors'
-
-
2
module ActiveRecord
-
2
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
-
end
-
-
# \Fixtures are a way of organizing data that you want to test against; in short, sample data.
-
#
-
# They are stored in YAML files, one file per model, which are placed in the directory
-
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
-
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
-
# The fixture file ends with the +.yml+ file extension, for example:
-
# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
-
#
-
# The format of a fixture file looks like this:
-
#
-
# rubyonrails:
-
# id: 1
-
# name: Ruby on Rails
-
# url: http://www.rubyonrails.org
-
#
-
# google:
-
# id: 2
-
# name: Google
-
# url: http://www.google.com
-
#
-
# This fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and
-
# is followed by an indented list of key/value pairs in the "key: value" format. Records are
-
# separated by a blank line for your viewing pleasure.
-
#
-
# Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
-
# See http://yaml.org/type/omap.html
-
# for the specification. You will need ordered fixtures when you have foreign key constraints
-
# on keys in the same table. This is commonly needed for tree structures. Example:
-
#
-
# --- !omap
-
# - parent:
-
# id: 1
-
# parent_id: NULL
-
# title: Parent
-
# - child:
-
# id: 2
-
# parent_id: 1
-
# title: Child
-
#
-
# = Using Fixtures in Test Cases
-
#
-
# Since fixtures are a testing construct, we use them in our unit and functional tests. There
-
# are two ways to use the fixtures, but first let's take a look at a sample unit test:
-
#
-
# require 'test_helper'
-
#
-
# class WebSiteTest < ActiveSupport::TestCase
-
# test "web_site_count" do
-
# assert_equal 2, WebSite.count
-
# end
-
# end
-
#
-
# By default, +test_helper.rb+ will load all of your fixtures into your test
-
# database, so this test will succeed.
-
#
-
# The testing environment will automatically load the all fixtures into the database before each
-
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
-
#
-
# In addition to being available in the database, the fixture's data may also be accessed by
-
# using a special dynamic method, which has the same name as the model, and accepts the
-
# name of the fixture to instantiate:
-
#
-
# test "find" do
-
# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
-
# end
-
#
-
# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the
-
# following tests:
-
#
-
# test "find_alt_method_1" do
-
# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
-
# end
-
#
-
# test "find_alt_method_2" do
-
# assert_equal "Ruby on Rails", @rubyonrails.name
-
# end
-
#
-
# In order to use these methods to access fixtured data within your testcases, you must specify one of the
-
# following in your <tt>ActiveSupport::TestCase</tt>-derived class:
-
#
-
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
-
# self.use_instantiated_fixtures = true
-
#
-
# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
-
# self.use_instantiated_fixtures = :no_instances
-
#
-
# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
-
# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
-
# large sets of fixtured data.
-
#
-
# = Dynamic fixtures with ERB
-
#
-
# Some times you don't care about the content of the fixtures as much as you care about the volume.
-
# In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load
-
# testing, like:
-
#
-
# <% 1.upto(1000) do |i| %>
-
# fix_<%= i %>:
-
# id: <%= i %>
-
# name: guy_<%= 1 %>
-
# <% end %>
-
#
-
# This will create 1000 very simple fixtures.
-
#
-
# Using ERB, you can also inject dynamic values into your fixtures with inserts like
-
# <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
-
# This is however a feature to be used with some caution. The point of fixtures are that they're
-
# stable units of predictable sample data. If you feel that you need to inject dynamic values, then
-
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
-
# in fixtures are to be considered a code smell.
-
#
-
# Helper methods defined in a fixture will not be available in other fixtures, to prevent against
-
# unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
-
# that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
-
#
-
# - define a helper method in `test_helper.rb`
-
# module FixtureFileHelpers
-
# def file_sha(path)
-
# Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
-
# end
-
# end
-
# ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
-
#
-
# - use the helper method in a fixture
-
# photo:
-
# name: kitten.png
-
# sha: <%= file_sha 'files/kitten.png' %>
-
#
-
# = Transactional Fixtures
-
#
-
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
-
# delete+insert for every test case.
-
#
-
# class FooTest < ActiveSupport::TestCase
-
# self.use_transactional_fixtures = true
-
#
-
# test "godzilla" do
-
# assert !Foo.all.empty?
-
# Foo.destroy_all
-
# assert Foo.all.empty?
-
# end
-
#
-
# test "godzilla aftermath" do
-
# assert !Foo.all.empty?
-
# end
-
# end
-
#
-
# If you preload your test database with all fixture data (probably in the rake task) and use
-
# transactional fixtures, then you may omit all fixtures declarations in your test cases since
-
# all the data's already there and every case rolls back its changes.
-
#
-
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
-
# true. This will provide access to fixture data for every table that has been loaded through
-
# fixtures (depending on the value of +use_instantiated_fixtures+).
-
#
-
# When *not* to use transactional fixtures:
-
#
-
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until
-
# all parent transactions commit, particularly, the fixtures transaction which is begun in setup
-
# and rolled back in teardown. Thus, you won't be able to verify
-
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
-
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
-
# Use InnoDB, MaxDB, or NDB instead.
-
#
-
# = Advanced Fixtures
-
#
-
# Fixtures that don't specify an ID get some extra features:
-
#
-
# * Stable, autogenerated IDs
-
# * Label references for associations (belongs_to, has_one, has_many)
-
# * HABTM associations as inline lists
-
#
-
# There are some more advanced features available even if the id is specified:
-
#
-
# * Autofilled timestamp columns
-
# * Fixture label interpolation
-
# * Support for YAML defaults
-
#
-
# == Stable, Autogenerated IDs
-
#
-
# Here, have a monkey fixture:
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
#
-
# reginald:
-
# id: 2
-
# name: Reginald the Pirate
-
#
-
# Each of these fixtures has two unique identifiers: one for the database
-
# and one for the humans. Why don't we generate the primary key instead?
-
# Hashing each fixture's label yields a consistent ID:
-
#
-
# george: # generated id: 503576764
-
# name: George the Monkey
-
#
-
# reginald: # generated id: 324201669
-
# name: Reginald the Pirate
-
#
-
# Active Record looks at the fixture's model class, discovers the correct
-
# primary key, and generates it right before inserting the fixture
-
# into the database.
-
#
-
# The generated ID for a given label is constant, so we can discover
-
# any fixture's ID without loading anything, as long as we know the label.
-
#
-
# == Label references for associations (belongs_to, has_one, has_many)
-
#
-
# Specifying foreign keys in fixtures can be very fragile, not to
-
# mention difficult to read. Since Active Record can figure out the ID of
-
# any fixture from its label, you can specify FK's by label instead of ID.
-
#
-
# === belongs_to
-
#
-
# Let's break out some more monkeys and pirates.
-
#
-
# ### in pirates.yml
-
#
-
# reginald:
-
# id: 1
-
# name: Reginald the Pirate
-
# monkey_id: 1
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
# pirate_id: 1
-
#
-
# Add a few more monkeys and pirates and break this into multiple files,
-
# and it gets pretty hard to keep track of what's going on. Let's
-
# use labels instead of IDs:
-
#
-
# ### in pirates.yml
-
#
-
# reginald:
-
# name: Reginald the Pirate
-
# monkey: george
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# name: George the Monkey
-
# pirate: reginald
-
#
-
# Pow! All is made clear. Active Record reflects on the fixture's model class,
-
# finds all the +belongs_to+ associations, and allows you to specify
-
# a target *label* for the *association* (monkey: george) rather than
-
# a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
-
#
-
# ==== Polymorphic belongs_to
-
#
-
# Supporting polymorphic relationships is a little bit more complicated, since
-
# Active Record needs to know what type your association is pointing at. Something
-
# like this should look familiar:
-
#
-
# ### in fruit.rb
-
#
-
# belongs_to :eater, polymorphic: true
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# id: 1
-
# name: apple
-
# eater_id: 1
-
# eater_type: Monkey
-
#
-
# Can we do better? You bet!
-
#
-
# apple:
-
# eater: george (Monkey)
-
#
-
# Just provide the polymorphic target type and Active Record will take care of the rest.
-
#
-
# === has_and_belongs_to_many
-
#
-
# Time to give our monkey some fruit.
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# id: 1
-
# name: apple
-
#
-
# orange:
-
# id: 2
-
# name: orange
-
#
-
# grape:
-
# id: 3
-
# name: grape
-
#
-
# ### in fruits_monkeys.yml
-
#
-
# apple_george:
-
# fruit_id: 1
-
# monkey_id: 1
-
#
-
# orange_george:
-
# fruit_id: 2
-
# monkey_id: 1
-
#
-
# grape_george:
-
# fruit_id: 3
-
# monkey_id: 1
-
#
-
# Let's make the HABTM fixture go away.
-
#
-
# ### in monkeys.yml
-
#
-
# george:
-
# id: 1
-
# name: George the Monkey
-
# fruits: apple, orange, grape
-
#
-
# ### in fruits.yml
-
#
-
# apple:
-
# name: apple
-
#
-
# orange:
-
# name: orange
-
#
-
# grape:
-
# name: grape
-
#
-
# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
-
# on George's fixture, but we could've just as easily specified a list
-
# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
-
# the fixture's model class and discovers the +has_and_belongs_to_many+
-
# associations.
-
#
-
# == Autofilled Timestamp Columns
-
#
-
# If your table/model specifies any of Active Record's
-
# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
-
# they will automatically be set to <tt>Time.now</tt>.
-
#
-
# If you've set specific values, they'll be left alone.
-
#
-
# == Fixture label interpolation
-
#
-
# The label of the current fixture is always available as a column value:
-
#
-
# geeksomnia:
-
# name: Geeksomnia's Account
-
# subdomain: $LABEL
-
# email: $LABEL@email.com
-
#
-
# Also, sometimes (like when porting older join table fixtures) you'll need
-
# to be able to get a hold of the identifier for a given label. ERB
-
# to the rescue:
-
#
-
# george_reginald:
-
# monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
-
# pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>
-
#
-
# == Support for YAML defaults
-
#
-
# You can set and reuse defaults in your fixtures YAML file.
-
# This is the same technique used in the +database.yml+ file to specify
-
# defaults:
-
#
-
# DEFAULTS: &DEFAULTS
-
# created_on: <%= 3.weeks.ago.to_s(:db) %>
-
#
-
# first:
-
# name: Smurf
-
# <<: *DEFAULTS
-
#
-
# second:
-
# name: Fraggle
-
# <<: *DEFAULTS
-
#
-
# Any fixture labeled "DEFAULTS" is safely ignored.
-
2
class FixtureSet
-
#--
-
# An instance of FixtureSet is normally stored in a single YAML file and
-
# possibly in a folder with the same name.
-
#++
-
-
2
MAX_ID = 2 ** 30 - 1
-
-
4
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
-
-
2
def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
-
10
config.pluralize_table_names ?
-
fixture_set_name.singularize.camelize :
-
fixture_set_name.camelize
-
end
-
-
2
def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
-
"#{ config.table_name_prefix }"\
-
"#{ fixture_set_name.tr('/', '_') }"\
-
"#{ config.table_name_suffix }".to_sym
-
end
-
-
2
def self.reset_cache
-
@@all_cached_fixtures.clear
-
end
-
-
2
def self.cache_for_connection(connection)
-
50
@@all_cached_fixtures[connection]
-
end
-
-
2
def self.fixture_is_cached?(connection, table_name)
-
40
cache_for_connection(connection)[table_name]
-
end
-
-
2
def self.cached_fixtures(connection, keys_to_fetch = nil)
-
8
if keys_to_fetch
-
8
cache_for_connection(connection).values_at(*keys_to_fetch)
-
else
-
cache_for_connection(connection).values
-
end
-
end
-
-
2
def self.cache_fixtures(connection, fixtures_map)
-
2
cache_for_connection(connection).update(fixtures_map)
-
end
-
-
2
def self.instantiate_fixtures(object, fixture_set, load_instances = true)
-
if load_instances
-
fixture_set.each do |fixture_name, fixture|
-
begin
-
object.instance_variable_set "@#{fixture_name}", fixture.find
-
rescue FixtureClassNotFound
-
nil
-
end
-
end
-
end
-
end
-
-
2
def self.instantiate_all_loaded_fixtures(object, load_instances = true)
-
all_loaded_fixtures.each_value do |fixture_set|
-
instantiate_fixtures(object, fixture_set, load_instances)
-
end
-
end
-
-
2
cattr_accessor :all_loaded_fixtures
-
2
self.all_loaded_fixtures = {}
-
-
2
class ClassCache
-
2
def initialize(class_names, config)
-
8
@class_names = class_names.stringify_keys
-
8
@config = config
-
-
# Remove string values that aren't constants or subclasses of AR
-
8
@class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
-
end
-
-
2
def [](fs_name)
-
10
@class_names.fetch(fs_name) {
-
10
klass = default_fixture_model(fs_name, @config).safe_constantize
-
10
insert_class(@class_names, fs_name, klass)
-
}
-
end
-
-
2
private
-
-
2
def insert_class(class_names, name, klass)
-
# We only want to deal with AR objects.
-
10
if klass && klass < ActiveRecord::Base
-
10
class_names[name] = klass
-
else
-
class_names[name] = nil
-
end
-
end
-
-
2
def default_fixture_model(fs_name, config)
-
10
ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
-
end
-
end
-
-
2
def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
-
8
fixture_set_names = Array(fixture_set_names).map(&:to_s)
-
8
class_names = ClassCache.new class_names, config
-
-
# FIXME: Apparently JK uses this.
-
8
connection = block_given? ? yield : ActiveRecord::Base.connection
-
-
8
files_to_read = fixture_set_names.reject { |fs_name|
-
40
fixture_is_cached?(connection, fs_name)
-
}
-
-
8
unless files_to_read.empty?
-
2
connection.disable_referential_integrity do
-
2
fixtures_map = {}
-
-
2
fixture_sets = files_to_read.map do |fs_name|
-
10
klass = class_names[fs_name]
-
10
conn = klass ? klass.connection : connection
-
10
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
-
conn,
-
fs_name,
-
klass,
-
::File.join(fixtures_directory, fs_name))
-
end
-
-
2
update_all_loaded_fixtures fixtures_map
-
-
2
connection.transaction(:requires_new => true) do
-
2
fixture_sets.each do |fs|
-
10
conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
-
10
table_rows = fs.table_rows
-
-
10
table_rows.each_key do |table|
-
18
conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
-
end
-
-
10
table_rows.each do |fixture_set_name, rows|
-
18
rows.each do |row|
-
52
conn.insert_fixture(row, fixture_set_name)
-
end
-
end
-
-
# Cap primary key sequences to max(pk).
-
10
if conn.respond_to?(:reset_pk_sequence!)
-
conn.reset_pk_sequence!(fs.table_name)
-
end
-
end
-
end
-
-
2
cache_fixtures(connection, fixtures_map)
-
end
-
end
-
8
cached_fixtures(connection, fixture_set_names)
-
end
-
-
# Returns a consistent, platform-independent identifier for +label+.
-
# Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
-
2
def self.identify(label, column_type = :integer)
-
76
if column_type == :uuid
-
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
-
else
-
76
Zlib.crc32(label.to_s) % MAX_ID
-
end
-
end
-
-
# Superclass for the evaluation contexts used by ERB fixtures.
-
2
def self.context_class
-
10
@context_class ||= Class.new
-
end
-
-
2
def self.update_all_loaded_fixtures(fixtures_map) # :nodoc:
-
2
all_loaded_fixtures.update(fixtures_map)
-
end
-
-
2
attr_reader :table_name, :name, :fixtures, :model_class, :config
-
-
2
def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
-
10
@name = name
-
10
@path = path
-
10
@config = config
-
10
@model_class = nil
-
-
10
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
-
10
@model_class = class_name
-
else
-
@model_class = class_name.safe_constantize if class_name
-
end
-
-
10
@connection = connection
-
-
10
@table_name = ( model_class.respond_to?(:table_name) ?
-
model_class.table_name :
-
self.class.default_fixture_table_name(name, config) )
-
-
10
@fixtures = read_fixture_files path, @model_class
-
end
-
-
2
def [](x)
-
152
fixtures[x]
-
end
-
-
2
def []=(k,v)
-
fixtures[k] = v
-
end
-
-
2
def each(&block)
-
fixtures.each(&block)
-
end
-
-
2
def size
-
fixtures.size
-
end
-
-
# Returns a hash of rows to be inserted. The key is the table, the value is
-
# a list of rows to insert to that table.
-
2
def table_rows
-
10
now = config.default_timezone == :utc ? Time.now.utc : Time.now
-
10
now = now.to_s(:db)
-
-
# allow a standard key to be used for doing defaults in YAML
-
10
fixtures.delete('DEFAULTS')
-
-
# track any join tables we need to insert later
-
18
rows = Hash.new { |h,table| h[table] = [] }
-
-
10
rows[table_name] = fixtures.map do |label, fixture|
-
28
row = fixture.to_hash
-
-
28
if model_class
-
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
-
28
if model_class.record_timestamps
-
28
timestamp_column_names.each do |c_name|
-
56
row[c_name] = now unless row.key?(c_name)
-
end
-
end
-
-
# interpolate the fixture label
-
28
row.each do |key, value|
-
168
row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String)
-
end
-
-
# generate a primary key if necessary
-
28
if has_primary_key_column? && !row.include?(primary_key_name)
-
28
row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
-
end
-
-
# If STI is used, find the correct subclass for association reflection
-
28
reflection_class =
-
if row.include?(inheritance_column_name)
-
row[inheritance_column_name].constantize rescue model_class
-
else
-
28
model_class
-
end
-
-
28
reflection_class._reflections.each_value do |association|
-
68
case association.macro
-
when :belongs_to
-
# Do not replace association name with association foreign key if they are named the same
-
24
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
-
-
24
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
-
24
if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
-
# support polymorphic belongs_to as "label (Type)"
-
row[association.foreign_type] = $1
-
end
-
-
24
fk_type = reflection_class.columns_hash[fk_name].type
-
24
row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
-
end
-
when :has_many
-
44
if association.options[:through]
-
22
add_join_records(rows, row, HasManyThroughProxy.new(association))
-
end
-
end
-
end
-
end
-
-
28
row
-
end
-
10
rows
-
end
-
-
2
class ReflectionProxy # :nodoc:
-
2
def initialize(association)
-
22
@association = association
-
end
-
-
2
def join_table
-
@association.join_table
-
end
-
-
2
def name
-
22
@association.name
-
end
-
-
2
def primary_key_type
-
18
@association.klass.column_types[@association.klass.primary_key].type
-
end
-
end
-
-
2
class HasManyThroughProxy < ReflectionProxy # :nodoc:
-
2
def rhs_key
-
18
@association.foreign_key
-
end
-
-
2
def lhs_key
-
18
@association.through_reflection.foreign_key
-
end
-
-
2
def join_table
-
18
@association.through_reflection.table_name
-
end
-
end
-
-
2
private
-
2
def primary_key_name
-
100
@primary_key_name ||= model_class && model_class.primary_key
-
end
-
-
2
def primary_key_type
-
28
@primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type
-
end
-
-
2
def add_join_records(rows, row, association)
-
# This is the case when the join table has no fixtures file
-
22
if (targets = row.delete(association.name.to_s))
-
18
table_name = association.join_table
-
18
column_type = association.primary_key_type
-
18
lhs_key = association.lhs_key
-
18
rhs_key = association.rhs_key
-
-
18
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
-
18
rows[table_name].concat targets.map { |target|
-
{ lhs_key => row[primary_key_name],
-
24
rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
-
}
-
end
-
end
-
-
2
def has_primary_key_column?
-
@has_primary_key_column ||= primary_key_name &&
-
38
model_class.columns.any? { |c| c.name == primary_key_name }
-
end
-
-
2
def timestamp_column_names
-
@timestamp_column_names ||=
-
28
%w(created_at created_on updated_at updated_on) & column_names
-
end
-
-
2
def inheritance_column_name
-
28
@inheritance_column_name ||= model_class && model_class.inheritance_column
-
end
-
-
2
def column_names
-
96
@column_names ||= @connection.columns(@table_name).collect { |c| c.name }
-
end
-
-
2
def read_fixture_files(path, model_class)
-
10
yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
-
::File.file?(f)
-
} + [yaml_file_path(path)]
-
-
10
yaml_files.each_with_object({}) do |file, fixtures|
-
10
FixtureSet::File.open(file) do |fh|
-
10
fh.each do |fixture_name, row|
-
28
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
-
end
-
end
-
end
-
end
-
-
2
def yaml_file_path(path)
-
10
"#{path}.yml"
-
end
-
-
end
-
-
#--
-
# Deprecate 'Fixtures' in favor of 'FixtureSet'.
-
#++
-
# :nodoc:
-
2
Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet')
-
-
2
class Fixture #:nodoc:
-
2
include Enumerable
-
-
2
class FixtureError < StandardError #:nodoc:
-
end
-
-
2
class FormatError < FixtureError #:nodoc:
-
end
-
-
2
attr_reader :model_class, :fixture
-
-
2
def initialize(fixture, model_class)
-
28
@fixture = fixture
-
28
@model_class = model_class
-
end
-
-
2
def class_name
-
model_class.name if model_class
-
end
-
-
2
def each
-
fixture.each { |item| yield item }
-
end
-
-
2
def [](key)
-
fixture[key]
-
end
-
-
2
alias :to_hash :fixture
-
-
2
def find
-
62
if model_class
-
62
model_class.unscoped do
-
62
model_class.find(fixture[model_class.primary_key])
-
end
-
else
-
raise FixtureClassNotFound, "No class attached to find."
-
end
-
end
-
end
-
end
-
-
2
module ActiveRecord
-
2
module TestFixtures
-
2
extend ActiveSupport::Concern
-
-
2
def before_setup
-
39
setup_fixtures
-
39
super
-
end
-
-
2
def after_teardown
-
39
super
-
39
teardown_fixtures
-
end
-
-
2
included do
-
2
class_attribute :fixture_path, :instance_writer => false
-
2
class_attribute :fixture_table_names
-
2
class_attribute :fixture_class_names
-
2
class_attribute :use_transactional_fixtures
-
2
class_attribute :use_instantiated_fixtures # true, false, or :no_instances
-
2
class_attribute :pre_loaded_fixtures
-
2
class_attribute :config
-
-
2
self.fixture_table_names = []
-
2
self.use_transactional_fixtures = true
-
2
self.use_instantiated_fixtures = false
-
2
self.pre_loaded_fixtures = false
-
2
self.config = ActiveRecord::Base
-
-
2
self.fixture_class_names = Hash.new do |h, fixture_set_name|
-
h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
-
end
-
end
-
-
2
module ClassMethods
-
# Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
-
#
-
# Examples:
-
#
-
# set_fixture_class some_fixture: SomeModel,
-
# 'namespaced/fixture' => Another::Model
-
#
-
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
-
2
def set_fixture_class(class_names = {})
-
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
-
end
-
-
2
def fixtures(*fixture_set_names)
-
6
if fixture_set_names.first == :all
-
2
fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
-
12
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
-
else
-
8
fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
-
end
-
-
6
self.fixture_table_names |= fixture_set_names
-
6
setup_fixture_accessors(fixture_set_names)
-
end
-
-
2
def setup_fixture_accessors(fixture_set_names = nil)
-
6
fixture_set_names = Array(fixture_set_names || fixture_table_names)
-
6
methods = Module.new do
-
6
fixture_set_names.each do |fs_name|
-
14
fs_name = fs_name.to_s
-
14
accessor_name = fs_name.tr('/', '_').to_sym
-
-
14
define_method(accessor_name) do |*fixture_names|
-
90
force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
-
-
90
@fixture_cache[fs_name] ||= {}
-
-
90
instances = fixture_names.map do |f_name|
-
90
f_name = f_name.to_s
-
90
@fixture_cache[fs_name].delete(f_name) if force_reload
-
-
90
if @loaded_fixtures[fs_name][f_name]
-
90
@fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
-
else
-
raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
-
end
-
end
-
-
86
instances.size == 1 ? instances.first : instances
-
end
-
14
private accessor_name
-
end
-
end
-
6
include methods
-
end
-
-
2
def uses_transaction(*methods)
-
@uses_transaction = [] unless defined?(@uses_transaction)
-
@uses_transaction.concat methods.map { |m| m.to_s }
-
end
-
-
2
def uses_transaction?(method)
-
78
@uses_transaction = [] unless defined?(@uses_transaction)
-
78
@uses_transaction.include?(method.to_s)
-
end
-
end
-
-
2
def run_in_transaction?
-
use_transactional_fixtures &&
-
78
!self.class.uses_transaction?(method_name)
-
end
-
-
2
def setup_fixtures(config = ActiveRecord::Base)
-
39
if pre_loaded_fixtures && !use_transactional_fixtures
-
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
-
end
-
-
39
@fixture_cache = {}
-
39
@fixture_connections = []
-
39
@@already_loaded_fixtures ||= {}
-
-
# Load fixtures once and begin transaction.
-
39
if run_in_transaction?
-
39
if @@already_loaded_fixtures[self.class]
-
31
@loaded_fixtures = @@already_loaded_fixtures[self.class]
-
else
-
8
@loaded_fixtures = load_fixtures(config)
-
8
@@already_loaded_fixtures[self.class] = @loaded_fixtures
-
end
-
39
@fixture_connections = enlist_fixture_connections
-
39
@fixture_connections.each do |connection|
-
39
connection.begin_transaction joinable: false
-
end
-
# Load fixtures for every test.
-
else
-
ActiveRecord::FixtureSet.reset_cache
-
@@already_loaded_fixtures[self.class] = nil
-
@loaded_fixtures = load_fixtures(config)
-
end
-
-
# Instantiate fixtures for every test if requested.
-
39
instantiate_fixtures if use_instantiated_fixtures
-
end
-
-
2
def teardown_fixtures
-
# Rollback changes if a transaction is active.
-
39
if run_in_transaction?
-
39
@fixture_connections.each do |connection|
-
39
connection.rollback_transaction if connection.transaction_open?
-
end
-
39
@fixture_connections.clear
-
else
-
ActiveRecord::FixtureSet.reset_cache
-
end
-
-
39
ActiveRecord::Base.clear_active_connections!
-
end
-
-
2
def enlist_fixture_connections
-
39
ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
-
end
-
-
2
private
-
2
def load_fixtures(config)
-
8
fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
-
48
Hash[fixtures.map { |f| [f.name, f] }]
-
end
-
-
2
def instantiate_fixtures
-
if pre_loaded_fixtures
-
raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
-
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
-
else
-
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
-
@loaded_fixtures.each_value do |fixture_set|
-
ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?)
-
end
-
end
-
end
-
-
2
def load_instances?
-
use_instantiated_fixtures != :no_instances
-
end
-
end
-
end
-
-
2
class ActiveRecord::FixtureSet::RenderContext # :nodoc:
-
2
def self.create_subclass
-
10
Class.new ActiveRecord::FixtureSet.context_class do
-
10
def get_binding
-
10
binding()
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
-
2
module ActiveRecord
-
# == Single table inheritance
-
#
-
# Active Record allows inheritance by storing the name of the class in a column that by
-
# default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
-
# This means that an inheritance looking like this:
-
#
-
# class Company < ActiveRecord::Base; end
-
# class Firm < Company; end
-
# class Client < Company; end
-
# class PriorityClient < Client; end
-
#
-
# When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
-
# the companies table with type = "Firm". You can then fetch this row again using
-
# <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
-
#
-
# Be aware that because the type column is an attribute on the record every new
-
# subclass will instantly be marked as dirty and the type column will be included
-
# in the list of changed attributes on the record. This is different from non
-
# STI classes:
-
#
-
# Company.new.changed? # => false
-
# Firm.new.changed? # => true
-
# Firm.new.changes # => {"type"=>["","Firm"]}
-
#
-
# If you don't have a type column defined in your table, single-table inheritance won't
-
# be triggered. In that case, it'll work just like normal subclasses with no special magic
-
# for differentiating between them or reloading the right type with find.
-
#
-
# Note, all the attributes for all the cases are kept in the same table. Read more:
-
# http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
-
#
-
2
module Inheritance
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
# Determines whether to store the full constant name including namespace when using STI.
-
2
class_attribute :store_full_sti_class, instance_writer: false
-
2
self.store_full_sti_class = true
-
end
-
-
2
module ClassMethods
-
# Determines if one of the attributes passed in is the inheritance column,
-
# and if the inheritance column is attr accessible, it initializes an
-
# instance of the given subclass instead of the base class.
-
2
def new(*args, &block)
-
21
if abstract_class? || self == Base
-
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
-
end
-
-
21
attrs = args.first
-
21
if subclass_from_attributes?(attrs)
-
subclass = subclass_from_attributes(attrs)
-
end
-
-
21
if subclass
-
subclass.new(*args, &block)
-
else
-
21
super
-
end
-
end
-
-
# Returns +true+ if this does not need STI type condition. Returns
-
# +false+ if STI type condition needs to be applied.
-
2
def descends_from_active_record?
-
9
if self == Base
-
false
-
9
elsif superclass.abstract_class?
-
superclass.descends_from_active_record?
-
else
-
9
superclass == Base || !columns_hash.include?(inheritance_column)
-
end
-
end
-
-
2
def finder_needs_type_condition? #:nodoc:
-
# This is like this because benchmarking justifies the strange :false stuff
-
261
:true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
-
end
-
-
2
def symbolized_base_class
-
ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement.')
-
@symbolized_base_class ||= base_class.to_s.to_sym
-
end
-
-
2
def symbolized_sti_name
-
ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_sti_name` is deprecated and will be removed without replacement.')
-
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
-
end
-
-
# Returns the class descending directly from ActiveRecord::Base, or
-
# an abstract class, if any, in the inheritance hierarchy.
-
#
-
# If A extends AR::Base, A.base_class will return A. If B descends from A
-
# through some arbitrarily deep hierarchy, B.base_class will return A.
-
#
-
# If B < A and C < B and if A is an abstract_class then both B.base_class
-
# and C.base_class would return B as the answer since A is an abstract_class.
-
2
def base_class
-
763
unless self < Base
-
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
-
end
-
-
763
if superclass == Base || superclass.abstract_class?
-
763
self
-
else
-
superclass.base_class
-
end
-
end
-
-
# Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
-
# If you are using inheritance with ActiveRecord and don't want child classes
-
# to utilize the implied STI table name of the parent class, this will need to be true.
-
# For example, given the following:
-
#
-
# class SuperClass < ActiveRecord::Base
-
# self.abstract_class = true
-
# end
-
# class Child < SuperClass
-
# self.table_name = 'the_table_i_really_want'
-
# end
-
#
-
#
-
# <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
-
#
-
2
attr_accessor :abstract_class
-
-
# Returns whether this class is an abstract class or not.
-
2
def abstract_class?
-
138
defined?(@abstract_class) && @abstract_class == true
-
end
-
-
2
def sti_name
-
store_full_sti_class ? name : name.demodulize
-
end
-
-
2
protected
-
-
# Returns the class type of the record using the current module as a prefix. So descendants of
-
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
-
2
def compute_type(type_name)
-
18
if type_name.match(/^::/)
-
# If the type is prefixed with a scope operator then we assume that
-
# the type_name is an absolute reference.
-
ActiveSupport::Dependencies.constantize(type_name)
-
else
-
# Build a list of candidates to search for
-
18
candidates = []
-
36
name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
-
18
candidates << type_name
-
-
18
candidates.each do |candidate|
-
36
constant = ActiveSupport::Dependencies.safe_constantize(candidate)
-
36
return constant if candidate == constant.to_s
-
end
-
-
raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
-
end
-
end
-
-
2
private
-
-
# Called by +instantiate+ to decide which class to use for a new
-
# record instance. For single-table inheritance, we check the record
-
# for a +type+ column and return the corresponding class.
-
2
def discriminate_class_for_record(record)
-
162
if using_single_table_inheritance?(record)
-
find_sti_class(record[inheritance_column])
-
else
-
162
super
-
end
-
end
-
-
2
def using_single_table_inheritance?(record)
-
162
record[inheritance_column].present? && columns_hash.include?(inheritance_column)
-
end
-
-
2
def find_sti_class(type_name)
-
if store_full_sti_class
-
ActiveSupport::Dependencies.constantize(type_name)
-
else
-
compute_type(type_name)
-
end
-
rescue NameError
-
raise SubclassNotFound,
-
"The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
-
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
-
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
-
"or overwrite #{name}.inheritance_column to use another column for that information."
-
end
-
-
2
def type_condition(table = arel_table)
-
sti_column = table[inheritance_column]
-
sti_names = ([self] + descendants).map { |model| model.sti_name }
-
-
sti_column.in(sti_names)
-
end
-
-
# Detect the subclass from the inheritance column of attrs. If the inheritance column value
-
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
-
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
-
# this will ignore the inheritance column and return nil
-
2
def subclass_from_attributes?(attrs)
-
21
columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
-
end
-
-
2
def subclass_from_attributes(attrs)
-
subclass_name = attrs.with_indifferent_access[inheritance_column]
-
-
if subclass_name.present? && subclass_name != self.name
-
subclass = subclass_name.safe_constantize
-
-
unless descendants.include?(subclass)
-
raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
-
end
-
-
subclass
-
end
-
end
-
end
-
-
2
def initialize_dup(other)
-
super
-
ensure_proper_type
-
end
-
-
2
private
-
-
2
def initialize_internals_callback
-
21
super
-
21
ensure_proper_type
-
end
-
-
# Sets the attribute used for single table inheritance to this class name if this is not the
-
# ActiveRecord::Base descendant.
-
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
-
# do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
-
# No such attribute would be set for objects of the Message class in that example.
-
2
def ensure_proper_type
-
21
klass = self.class
-
21
if klass.finder_needs_type_condition?
-
write_attribute(klass.inheritance_column, klass.sti_name)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
2
module Integration
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
##
-
# :singleton-method:
-
# Indicates the format used to generate the timestamp in the cache key.
-
# Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
-
#
-
# This is +:nsec+, by default.
-
2
class_attribute :cache_timestamp_format, :instance_writer => false
-
2
self.cache_timestamp_format = :nsec
-
end
-
-
# Returns a String, which Action Pack uses for constructing an URL to this
-
# object. The default implementation returns this record's id as a String,
-
# or nil if this record's unsaved.
-
#
-
# For example, suppose that you have a User model, and that you have a
-
# <tt>resources :users</tt> route. Normally, +user_path+ will
-
# construct a path with the user object's 'id' in it:
-
#
-
# user = User.find_by(name: 'Phusion')
-
# user_path(user) # => "/users/1"
-
#
-
# You can override +to_param+ in your model to make +user_path+ construct
-
# a path using the user's name instead of the user's id:
-
#
-
# class User < ActiveRecord::Base
-
# def to_param # overridden
-
# name
-
# end
-
# end
-
#
-
# user = User.find_by(name: 'Phusion')
-
# user_path(user) # => "/users/Phusion"
-
2
def to_param
-
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
-
64
id && id.to_s # Be sure to stringify the id for routes
-
end
-
-
# Returns a cache key that can be used to identify this record.
-
#
-
# Product.new.cache_key # => "products/new"
-
# Product.find(5).cache_key # => "products/5" (updated_at not available)
-
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
-
#
-
# You can also pass a list of named timestamps, and the newest in the list will be
-
# used to generate the key:
-
#
-
# Person.find(5).cache_key(:updated_at, :last_reviewed_at)
-
2
def cache_key(*timestamp_names)
-
case
-
when new_record?
-
"#{model_name.cache_key}/new"
-
when timestamp_names.any?
-
timestamp = max_updated_column_timestamp(timestamp_names)
-
timestamp = timestamp.utc.to_s(cache_timestamp_format)
-
"#{model_name.cache_key}/#{id}-#{timestamp}"
-
when timestamp = max_updated_column_timestamp
-
timestamp = timestamp.utc.to_s(cache_timestamp_format)
-
"#{model_name.cache_key}/#{id}-#{timestamp}"
-
else
-
"#{model_name.cache_key}/#{id}"
-
end
-
end
-
-
2
module ClassMethods
-
# Defines your model's +to_param+ method to generate "pretty" URLs
-
# using +method_name+, which can be any attribute or method that
-
# responds to +to_s+.
-
#
-
# class User < ActiveRecord::Base
-
# to_param :name
-
# end
-
#
-
# user = User.find_by(name: 'Fancy Pants')
-
# user.id # => 123
-
# user_path(user) # => "/users/123-fancy-pants"
-
#
-
# Values longer than 20 characters will be truncated. The value
-
# is truncated word by word.
-
#
-
# user = User.find_by(name: 'David HeinemeierHansson')
-
# user.id # => 125
-
# user_path(user) # => "/users/125-david"
-
#
-
# Because the generated param begins with the record's +id+, it is
-
# suitable for passing to +find+. In a controller, for example:
-
#
-
# params[:id] # => "123-fancy-pants"
-
# User.find(params[:id]).id # => 123
-
2
def to_param(method_name = nil)
-
if method_name.nil?
-
super()
-
else
-
define_method :to_param do
-
if (default = super()) &&
-
(result = send(method_name).to_s).present? &&
-
(param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
-
"#{default}-#{param}"
-
else
-
default
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module LegacyYamlAdapter
-
2
def self.convert(klass, coder)
-
162
return coder unless coder.is_a?(Psych::Coder)
-
-
case coder["active_record_yaml_version"]
-
when 0 then coder
-
else
-
if coder["attributes"].is_a?(AttributeSet)
-
coder
-
else
-
Rails41.convert(klass, coder)
-
end
-
end
-
end
-
-
2
module Rails41
-
2
def self.convert(klass, coder)
-
attributes = klass.attributes_builder
-
.build_from_database(coder["attributes"])
-
new_record = coder["attributes"][klass.primary_key].blank?
-
-
{
-
"attributes" => attributes,
-
"new_record" => new_record,
-
}
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Locking
-
# == What is Optimistic Locking
-
#
-
# Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
-
# conflicts with the data. It does this by checking whether another process has made changes to a record since
-
# it was opened, an <tt>ActiveRecord::StaleObjectError</tt> exception is thrown if that has occurred
-
# and the update is ignored.
-
#
-
# Check out <tt>ActiveRecord::Locking::Pessimistic</tt> for an alternative.
-
#
-
# == Usage
-
#
-
# Active Records support optimistic locking if the field +lock_version+ is present. Each update to the
-
# record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice
-
# will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
-
#
-
# p1 = Person.find(1)
-
# p2 = Person.find(1)
-
#
-
# p1.first_name = "Michael"
-
# p1.save
-
#
-
# p2.first_name = "should fail"
-
# p2.save # Raises a ActiveRecord::StaleObjectError
-
#
-
# Optimistic locking will also check for stale data when objects are destroyed. Example:
-
#
-
# p1 = Person.find(1)
-
# p2 = Person.find(1)
-
#
-
# p1.first_name = "Michael"
-
# p1.save
-
#
-
# p2.destroy # Raises a ActiveRecord::StaleObjectError
-
#
-
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
-
# or otherwise apply the business logic needed to resolve the conflict.
-
#
-
# This locking mechanism will function inside a single Ruby process. To make it work across all
-
# web requests, the recommended approach is to add +lock_version+ as a hidden field to your form.
-
#
-
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
-
# To override the name of the +lock_version+ column, set the <tt>locking_column</tt> class attribute:
-
#
-
# class Person < ActiveRecord::Base
-
# self.locking_column = :lock_person
-
# end
-
#
-
2
module Optimistic
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :lock_optimistically, instance_writer: false
-
2
self.lock_optimistically = true
-
end
-
-
2
def locking_enabled? #:nodoc:
-
16
self.class.locking_enabled?
-
end
-
-
2
private
-
2
def increment_lock
-
lock_col = self.class.locking_column
-
previous_lock_value = send(lock_col).to_i
-
send(lock_col + '=', previous_lock_value + 1)
-
end
-
-
2
def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
-
6
if locking_enabled?
-
# We always want to persist the locking version, even if we don't detect
-
# a change from the default, since the database might have no default
-
attribute_names |= [self.class.locking_column]
-
end
-
6
super
-
end
-
-
2
def _update_record(attribute_names = self.attribute_names) #:nodoc:
-
4
return super unless locking_enabled?
-
return 0 if attribute_names.empty?
-
-
lock_col = self.class.locking_column
-
previous_lock_value = send(lock_col).to_i
-
increment_lock
-
-
attribute_names += [lock_col]
-
attribute_names.uniq!
-
-
begin
-
relation = self.class.unscoped
-
-
affected_rows = relation.where(
-
self.class.primary_key => id,
-
lock_col => previous_lock_value,
-
).update_all(
-
Hash[attributes_for_update(attribute_names).map do |name|
-
[name, _read_attribute(name)]
-
end]
-
)
-
-
unless affected_rows == 1
-
raise ActiveRecord::StaleObjectError.new(self, "update")
-
end
-
-
affected_rows
-
-
# If something went wrong, revert the version.
-
rescue Exception
-
send(lock_col + '=', previous_lock_value)
-
raise
-
end
-
end
-
-
2
def destroy_row
-
3
affected_rows = super
-
-
3
if locking_enabled? && affected_rows != 1
-
raise ActiveRecord::StaleObjectError.new(self, "destroy")
-
end
-
-
3
affected_rows
-
end
-
-
2
def relation_for_destroy
-
3
relation = super
-
-
3
if locking_enabled?
-
column_name = self.class.locking_column
-
column = self.class.columns_hash[column_name]
-
substitute = self.class.connection.substitute_at(column)
-
-
relation = relation.where(self.class.arel_table[column_name].eq(substitute))
-
relation.bind_values << [column, self[column_name].to_i]
-
end
-
-
3
relation
-
end
-
-
2
module ClassMethods
-
2
DEFAULT_LOCKING_COLUMN = 'lock_version'
-
-
# Returns true if the +lock_optimistically+ flag is set to true
-
# (which it is, by default) and the table includes the
-
# +locking_column+ column (defaults to +lock_version+).
-
2
def locking_enabled?
-
16
lock_optimistically && columns_hash[locking_column]
-
end
-
-
# Set the column to use for optimistic locking. Defaults to +lock_version+.
-
2
def locking_column=(value)
-
12
clear_caches_calculated_from_columns
-
12
@locking_column = value.to_s
-
end
-
-
# The version column used for optimistic locking. Defaults to +lock_version+.
-
2
def locking_column
-
104
reset_locking_column unless defined?(@locking_column)
-
104
@locking_column
-
end
-
-
# Reset the column used for optimistic locking back to the +lock_version+ default.
-
2
def reset_locking_column
-
12
self.locking_column = DEFAULT_LOCKING_COLUMN
-
end
-
-
# Make sure the lock version column gets updated when counters are
-
# updated.
-
2
def update_counters(id, counters)
-
counters = counters.merge(locking_column => 1) if locking_enabled?
-
super
-
end
-
-
2
private
-
-
# We need to apply this decorator here, rather than on module inclusion. The closure
-
# created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
-
# sub class being decorated. As such, changes to `lock_optimistically`, or
-
# `locking_column` would not be picked up.
-
2
def inherited(subclass)
-
12
subclass.class_eval do
-
100
is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
-
12
decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
-
LockingType.new(type)
-
end
-
end
-
12
super
-
end
-
end
-
end
-
-
2
class LockingType < SimpleDelegator # :nodoc:
-
2
def type_cast_from_database(value)
-
# `nil` *should* be changed to 0
-
super.to_i
-
end
-
-
2
def init_with(coder)
-
__setobj__(coder['subtype'])
-
end
-
-
2
def encode_with(coder)
-
coder['subtype'] = __getobj__
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Locking
-
# Locking::Pessimistic provides support for row-level locking using
-
# SELECT ... FOR UPDATE and other lock types.
-
#
-
# Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
-
# lock on the selected rows:
-
# # select * from accounts where id=1 for update
-
# Account.lock.find(1)
-
#
-
# Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
-
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
-
#
-
# Account.transaction do
-
# # select * from accounts where name = 'shugo' limit 1 for update
-
# shugo = Account.where("name = 'shugo'").lock(true).first
-
# yuko = Account.where("name = 'yuko'").lock(true).first
-
# shugo.balance -= 100
-
# shugo.save!
-
# yuko.balance += 100
-
# yuko.save!
-
# end
-
#
-
# You can also use <tt>ActiveRecord::Base#lock!</tt> method to lock one record by id.
-
# This may be better if you don't need to lock every row. Example:
-
#
-
# Account.transaction do
-
# # select * from accounts where ...
-
# accounts = Account.where(...)
-
# account1 = accounts.detect { |account| ... }
-
# account2 = accounts.detect { |account| ... }
-
# # select * from accounts where id=? for update
-
# account1.lock!
-
# account2.lock!
-
# account1.balance -= 100
-
# account1.save!
-
# account2.balance += 100
-
# account2.save!
-
# end
-
#
-
# You can start a transaction and acquire the lock in one go by calling
-
# <tt>with_lock</tt> with a block. The block is called from within
-
# a transaction, the object is already locked. Example:
-
#
-
# account = Account.first
-
# account.with_lock do
-
# # This block is called within a transaction,
-
# # account is already locked.
-
# account.balance -= 100
-
# account.save!
-
# end
-
#
-
# Database-specific information on row locking:
-
# MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
-
# PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
-
2
module Pessimistic
-
# Obtain a row lock on this record. Reloads the record to obtain the requested
-
# lock. Pass an SQL locking clause to append the end of the SELECT statement
-
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
-
# the locked record.
-
2
def lock!(lock = true)
-
reload(:lock => lock) if persisted?
-
self
-
end
-
-
# Wraps the passed block in a transaction, locking the object
-
# before yielding. You can pass the SQL locking clause
-
# as argument (see <tt>lock!</tt>).
-
2
def with_lock(lock = true)
-
transaction do
-
lock!(lock)
-
yield
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
class LogSubscriber < ActiveSupport::LogSubscriber
-
2
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
-
-
2
def self.runtime=(value)
-
480
ActiveRecord::RuntimeRegistry.sql_runtime = value
-
end
-
-
2
def self.runtime
-
480
ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
-
end
-
-
2
def self.reset_runtime
-
90
rt, self.runtime = runtime, 0
-
90
rt
-
end
-
-
2
def initialize
-
2
super
-
2
@odd = false
-
end
-
-
2
def render_bind(column, value)
-
150
if column
-
150
if column.binary?
-
# This specifically deals with the PG adapter that casts bytea columns into a Hash.
-
value = value[:value] if value.is_a?(Hash)
-
value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
-
end
-
-
150
[column.name, value]
-
else
-
[nil, value]
-
end
-
end
-
-
2
def sql(event)
-
390
self.class.runtime += event.duration
-
390
return unless logger.debug?
-
-
390
payload = event.payload
-
-
390
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
-
-
354
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
-
354
sql = payload[:sql]
-
354
binds = nil
-
-
354
unless (payload[:binds] || []).empty?
-
129
binds = " " + payload[:binds].map { |col,v|
-
150
render_bind(col, v)
-
}.inspect
-
end
-
-
354
if odd?
-
178
name = color(name, CYAN, true)
-
178
sql = color(sql, nil, true)
-
else
-
176
name = color(name, MAGENTA, true)
-
end
-
-
354
debug " #{name} #{sql}#{binds}"
-
end
-
-
2
def odd?
-
354
@odd = !@odd
-
end
-
-
2
def logger
-
1878
ActiveRecord::Base.logger
-
end
-
end
-
end
-
-
2
ActiveRecord::LogSubscriber.attach_to :active_record
-
2
module ActiveRecord
-
2
module ModelSchema
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
##
-
# :singleton-method:
-
# Accessor for the prefix type that will be prepended to every primary key column name.
-
# The options are :table_name and :table_name_with_underscore. If the first is specified,
-
# the Product class will look for "productid" instead of "id" as the primary column. If the
-
# latter is specified, the Product class will look for "product_id" instead of "id". Remember
-
# that this is a global setting for all Active Records.
-
2
mattr_accessor :primary_key_prefix_type, instance_writer: false
-
-
##
-
# :singleton-method:
-
# Accessor for the name of the prefix string to prepend to every table name. So if set
-
# to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
-
# etc. This is a convenient way of creating a namespace for tables in a shared database.
-
# By default, the prefix is the empty string.
-
#
-
# If you are organising your models within modules you can add a prefix to the models within
-
# a namespace by defining a singleton method in the parent module called table_name_prefix which
-
# returns your chosen prefix.
-
2
class_attribute :table_name_prefix, instance_writer: false
-
2
self.table_name_prefix = ""
-
-
##
-
# :singleton-method:
-
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
-
# "people_basecamp"). By default, the suffix is the empty string.
-
#
-
# If you are organising your models within modules, you can add a suffix to the models within
-
# a namespace by defining a singleton method in the parent module called table_name_suffix which
-
# returns your chosen suffix.
-
2
class_attribute :table_name_suffix, instance_writer: false
-
2
self.table_name_suffix = ""
-
-
##
-
# :singleton-method:
-
# Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
-
2
class_attribute :schema_migrations_table_name, instance_accessor: false
-
2
self.schema_migrations_table_name = "schema_migrations"
-
-
##
-
# :singleton-method:
-
# Indicates whether table names should be the pluralized versions of the corresponding class names.
-
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
-
# See table_name for the full rules on table/class naming. This is true, by default.
-
2
class_attribute :pluralize_table_names, instance_writer: false
-
2
self.pluralize_table_names = true
-
-
2
self.inheritance_column = 'type'
-
-
2
delegate :type_for_attribute, to: :class
-
end
-
-
# Derives the join table name for +first_table+ and +second_table+. The
-
# table names appear in alphabetical order. A common prefix is removed
-
# (useful for namespaced models like Music::Artist and Music::Record):
-
#
-
# artists, records => artists_records
-
# records, artists => artists_records
-
# music_artists, music_records => music_artists_records
-
2
def self.derive_join_table_name(first_table, second_table) # :nodoc:
-
[first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
-
end
-
-
2
module ClassMethods
-
# Guesses the table name (in forced lower-case) based on the name of the class in the
-
# inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy
-
# looks like: Reply < Message < ActiveRecord::Base, then Message is used
-
# to guess the table name even when called on Reply. The rules used to do the guess
-
# are handled by the Inflector class in Active Support, which knows almost all common
-
# English inflections. You can add new inflections in config/initializers/inflections.rb.
-
#
-
# Nested classes are given table names prefixed by the singular form of
-
# the parent's table name. Enclosing modules are not considered.
-
#
-
# ==== Examples
-
#
-
# class Invoice < ActiveRecord::Base
-
# end
-
#
-
# file class table_name
-
# invoice.rb Invoice invoices
-
#
-
# class Invoice < ActiveRecord::Base
-
# class Lineitem < ActiveRecord::Base
-
# end
-
# end
-
#
-
# file class table_name
-
# invoice.rb Invoice::Lineitem invoice_lineitems
-
#
-
# module Invoice
-
# class Lineitem < ActiveRecord::Base
-
# end
-
# end
-
#
-
# file class table_name
-
# invoice/lineitem.rb Invoice::Lineitem lineitems
-
#
-
# Additionally, the class-level +table_name_prefix+ is prepended and the
-
# +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
-
# the table name guess for an Invoice class becomes "myapp_invoices".
-
# Invoice::Lineitem becomes "myapp_invoice_lineitems".
-
#
-
# You can also set your own table name explicitly:
-
#
-
# class Mouse < ActiveRecord::Base
-
# self.table_name = "mice"
-
# end
-
#
-
# Alternatively, you can override the table_name method to define your
-
# own computation. (Possibly using <tt>super</tt> to manipulate the default
-
# table name.) Example:
-
#
-
# class Post < ActiveRecord::Base
-
# def self.table_name
-
# "special_" + super
-
# end
-
# end
-
# Post.table_name # => "special_posts"
-
2
def table_name
-
264
reset_table_name unless defined?(@table_name)
-
264
@table_name
-
end
-
-
# Sets the table name explicitly. Example:
-
#
-
# class Project < ActiveRecord::Base
-
# self.table_name = "project"
-
# end
-
#
-
# You can also just define your own <tt>self.table_name</tt> method; see
-
# the documentation for ActiveRecord::Base#table_name.
-
2
def table_name=(value)
-
10
value = value && value.to_s
-
-
10
if defined?(@table_name)
-
return if value == @table_name
-
reset_column_information if connected?
-
end
-
-
10
@table_name = value
-
10
@quoted_table_name = nil
-
10
@arel_table = nil
-
10
@sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
-
10
@relation = Relation.create(self, arel_table)
-
end
-
-
# Returns a quoted version of the table name, used to construct SQL statements.
-
2
def quoted_table_name
-
@quoted_table_name ||= connection.quote_table_name(table_name)
-
end
-
-
# Computes the table name, (re)sets it internally, and returns it.
-
2
def reset_table_name #:nodoc:
-
10
self.table_name = if abstract_class?
-
superclass == Base ? nil : superclass.table_name
-
elsif superclass.abstract_class?
-
superclass.table_name || compute_table_name
-
else
-
10
compute_table_name
-
end
-
end
-
-
2
def full_table_name_prefix #:nodoc:
-
20
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
-
end
-
-
2
def full_table_name_suffix #:nodoc:
-
20
(parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
-
end
-
-
# Defines the name of the table column which will store the class name on single-table
-
# inheritance situations.
-
#
-
# The default inheritance column name is +type+, which means it's a
-
# reserved word inside Active Record. To be able to use single-table
-
# inheritance with another column name, or to use the column +type+ in
-
# your own model for something else, you can set +inheritance_column+:
-
#
-
# self.inheritance_column = 'zoink'
-
2
def inheritance_column
-
416
(@inheritance_column ||= nil) || superclass.inheritance_column
-
end
-
-
# Sets the value of inheritance_column
-
2
def inheritance_column=(value)
-
2
@inheritance_column = value.to_s
-
2
@explicit_inheritance_column = true
-
end
-
-
2
def sequence_name
-
if base_class == self
-
@sequence_name ||= reset_sequence_name
-
else
-
(@sequence_name ||= nil) || base_class.sequence_name
-
end
-
end
-
-
2
def reset_sequence_name #:nodoc:
-
@explicit_sequence_name = false
-
@sequence_name = connection.default_sequence_name(table_name, primary_key)
-
end
-
-
# Sets the name of the sequence to use when generating ids to the given
-
# value, or (if the value is nil or false) to the value returned by the
-
# given block. This is required for Oracle and is useful for any
-
# database which relies on sequences for primary key generation.
-
#
-
# If a sequence name is not explicitly set when using Oracle,
-
# it will default to the commonly used pattern of: #{table_name}_seq
-
#
-
# If a sequence name is not explicitly set when using PostgreSQL, it
-
# will discover the sequence corresponding to your primary key for you.
-
#
-
# class Project < ActiveRecord::Base
-
# self.sequence_name = "projectseq" # default would have been "project_seq"
-
# end
-
2
def sequence_name=(value)
-
@sequence_name = value.to_s
-
@explicit_sequence_name = true
-
end
-
-
# Indicates whether the table associated with this class exists
-
2
def table_exists?
-
13
connection.schema_cache.table_exists?(table_name)
-
end
-
-
2
def attributes_builder # :nodoc:
-
167
@attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key)
-
end
-
-
2
def column_types # :nodoc:
-
@column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
-
12
h.default = Type::Value.new
-
59
end
-
end
-
-
2
def type_for_attribute(attr_name) # :nodoc:
-
19
column_types[attr_name]
-
end
-
-
# Returns a hash where the keys are column names and the values are
-
# default values when instantiating the AR object for this table.
-
2
def column_defaults
-
_default_attributes.to_hash
-
end
-
-
2
def _default_attributes # :nodoc:
-
@default_attributes ||= attributes_builder.build_from_database(
-
21
raw_default_values)
-
end
-
-
# Returns an array of column names as strings.
-
2
def column_names
-
337
@column_names ||= columns.map { |column| column.name }
-
end
-
-
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
-
# and columns used for single table inheritance have been removed.
-
2
def content_columns
-
@content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
-
end
-
-
# Resets all the cached information about columns, which will cause them
-
# to be reloaded on the next request.
-
#
-
# The most common usage pattern for this method is probably in a migration,
-
# when just after creating a table you want to populate it with some default
-
# values, eg:
-
#
-
# class CreateJobLevels < ActiveRecord::Migration
-
# def up
-
# create_table :job_levels do |t|
-
# t.integer :id
-
# t.string :name
-
#
-
# t.timestamps
-
# end
-
#
-
# JobLevel.reset_column_information
-
# %w{assistant executive manager director}.each do |type|
-
# JobLevel.create(name: type)
-
# end
-
# end
-
#
-
# def down
-
# drop_table :job_levels
-
# end
-
# end
-
2
def reset_column_information
-
connection.clear_cache!
-
undefine_attribute_methods
-
connection.schema_cache.clear_table_cache!(table_name)
-
-
@arel_engine = nil
-
@column_names = nil
-
@column_types = nil
-
@content_columns = nil
-
@default_attributes = nil
-
@inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
-
@relation = nil
-
end
-
-
2
private
-
-
# Guesses the table name, but does not decorate it with prefix and suffix information.
-
2
def undecorated_table_name(class_name = base_class.name)
-
10
table_name = class_name.to_s.demodulize.underscore
-
10
pluralize_table_names ? table_name.pluralize : table_name
-
end
-
-
# Computes and returns a table name according to default conventions.
-
2
def compute_table_name
-
10
base = base_class
-
10
if self == base
-
# Nested classes are prefixed with singular parent table name.
-
10
if parent < Base && !parent.abstract_class?
-
contained = parent.table_name
-
contained = contained.singularize if parent.pluralize_table_names
-
contained += '_'
-
end
-
-
10
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
-
else
-
# STI subclasses always use their superclass' table.
-
base.table_name
-
end
-
end
-
-
2
def raw_default_values
-
5
columns_hash.transform_values(&:default)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/object/try'
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
-
2
module ActiveRecord
-
2
module NestedAttributes #:nodoc:
-
2
class TooManyRecords < ActiveRecordError
-
end
-
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :nested_attributes_options, instance_writer: false
-
2
self.nested_attributes_options = {}
-
end
-
-
# = Active Record Nested Attributes
-
#
-
# Nested attributes allow you to save attributes on associated records
-
# through the parent. By default nested attribute updating is turned off
-
# and you can enable it using the accepts_nested_attributes_for class
-
# method. When you enable nested attributes an attribute writer is
-
# defined on the model.
-
#
-
# The attribute writer is named after the association, which means that
-
# in the following example, two new methods are added to your model:
-
#
-
# <tt>author_attributes=(attributes)</tt> and
-
# <tt>pages_attributes=(attributes)</tt>.
-
#
-
# class Book < ActiveRecord::Base
-
# has_one :author
-
# has_many :pages
-
#
-
# accepts_nested_attributes_for :author, :pages
-
# end
-
#
-
# Note that the <tt>:autosave</tt> option is automatically enabled on every
-
# association that accepts_nested_attributes_for is used for.
-
#
-
# === One-to-one
-
#
-
# Consider a Member model that has one Avatar:
-
#
-
# class Member < ActiveRecord::Base
-
# has_one :avatar
-
# accepts_nested_attributes_for :avatar
-
# end
-
#
-
# Enabling nested attributes on a one-to-one association allows you to
-
# create the member and avatar in one go:
-
#
-
# params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
-
# member = Member.create(params[:member])
-
# member.avatar.id # => 2
-
# member.avatar.icon # => 'smiling'
-
#
-
# It also allows you to update the avatar through the member:
-
#
-
# params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
-
# member.update params[:member]
-
# member.avatar.icon # => 'sad'
-
#
-
# By default you will only be able to set and update attributes on the
-
# associated model. If you want to destroy the associated model through the
-
# attributes hash, you have to enable it first using the
-
# <tt>:allow_destroy</tt> option.
-
#
-
# class Member < ActiveRecord::Base
-
# has_one :avatar
-
# accepts_nested_attributes_for :avatar, allow_destroy: true
-
# end
-
#
-
# Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
-
# value that evaluates to +true+, you will destroy the associated model:
-
#
-
# member.avatar_attributes = { id: '2', _destroy: '1' }
-
# member.avatar.marked_for_destruction? # => true
-
# member.save
-
# member.reload.avatar # => nil
-
#
-
# Note that the model will _not_ be destroyed until the parent is saved.
-
#
-
# === One-to-many
-
#
-
# Consider a member that has a number of posts:
-
#
-
# class Member < ActiveRecord::Base
-
# has_many :posts
-
# accepts_nested_attributes_for :posts
-
# end
-
#
-
# You can now set or update attributes on the associated posts through
-
# an attribute hash for a member: include the key +:posts_attributes+
-
# with an array of hashes of post attributes as a value.
-
#
-
# For each hash that does _not_ have an <tt>id</tt> key a new record will
-
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
-
# that evaluates to +true+.
-
#
-
# params = { member: {
-
# name: 'joe', posts_attributes: [
-
# { title: 'Kari, the awesome Ruby documentation browser!' },
-
# { title: 'The egalitarian assumption of the modern citizen' },
-
# { title: '', _destroy: '1' } # this will be ignored
-
# ]
-
# }}
-
#
-
# member = Member.create(params[:member])
-
# member.posts.length # => 2
-
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
-
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
-
#
-
# You may also set a :reject_if proc to silently ignore any new record
-
# hashes if they fail to pass your criteria. For example, the previous
-
# example could be rewritten as:
-
#
-
# class Member < ActiveRecord::Base
-
# has_many :posts
-
# accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
-
# end
-
#
-
# params = { member: {
-
# name: 'joe', posts_attributes: [
-
# { title: 'Kari, the awesome Ruby documentation browser!' },
-
# { title: 'The egalitarian assumption of the modern citizen' },
-
# { title: '' } # this will be ignored because of the :reject_if proc
-
# ]
-
# }}
-
#
-
# member = Member.create(params[:member])
-
# member.posts.length # => 2
-
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
-
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
-
#
-
# Alternatively, :reject_if also accepts a symbol for using methods:
-
#
-
# class Member < ActiveRecord::Base
-
# has_many :posts
-
# accepts_nested_attributes_for :posts, reject_if: :new_record?
-
# end
-
#
-
# class Member < ActiveRecord::Base
-
# has_many :posts
-
# accepts_nested_attributes_for :posts, reject_if: :reject_posts
-
#
-
# def reject_posts(attributed)
-
# attributed['title'].blank?
-
# end
-
# end
-
#
-
# If the hash contains an <tt>id</tt> key that matches an already
-
# associated record, the matching record will be modified:
-
#
-
# member.attributes = {
-
# name: 'Joe',
-
# posts_attributes: [
-
# { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
-
# { id: 2, title: '[UPDATED] other post' }
-
# ]
-
# }
-
#
-
# member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
-
# member.posts.second.title # => '[UPDATED] other post'
-
#
-
# By default the associated records are protected from being destroyed. If
-
# you want to destroy any of the associated records through the attributes
-
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
-
# option. This will allow you to also use the <tt>_destroy</tt> key to
-
# destroy existing records:
-
#
-
# class Member < ActiveRecord::Base
-
# has_many :posts
-
# accepts_nested_attributes_for :posts, allow_destroy: true
-
# end
-
#
-
# params = { member: {
-
# posts_attributes: [{ id: '2', _destroy: '1' }]
-
# }}
-
#
-
# member.attributes = params[:member]
-
# member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
-
# member.posts.length # => 2
-
# member.save
-
# member.reload.posts.length # => 1
-
#
-
# Nested attributes for an associated collection can also be passed in
-
# the form of a hash of hashes instead of an array of hashes:
-
#
-
# Member.create(name: 'joe',
-
# posts_attributes: { first: { title: 'Foo' },
-
# second: { title: 'Bar' } })
-
#
-
# has the same effect as
-
#
-
# Member.create(name: 'joe',
-
# posts_attributes: [ { title: 'Foo' },
-
# { title: 'Bar' } ])
-
#
-
# The keys of the hash which is the value for +:posts_attributes+ are
-
# ignored in this case.
-
# However, it is not allowed to use +'id'+ or +:id+ for one of
-
# such keys, otherwise the hash will be wrapped in an array and
-
# interpreted as an attribute hash for a single post.
-
#
-
# Passing attributes for an associated collection in the form of a hash
-
# of hashes can be used with hashes generated from HTTP/HTML parameters,
-
# where there maybe no natural way to submit an array of hashes.
-
#
-
# === Saving
-
#
-
# All changes to models, including the destruction of those marked for
-
# destruction, are saved and destroyed automatically and atomically when
-
# the parent model is saved. This happens inside the transaction initiated
-
# by the parents save method. See ActiveRecord::AutosaveAssociation.
-
#
-
# === Validating the presence of a parent model
-
#
-
# If you want to validate that a child record is associated with a parent
-
# record, you can use <tt>validates_presence_of</tt> and
-
# <tt>inverse_of</tt> as this example illustrates:
-
#
-
# class Member < ActiveRecord::Base
-
# has_many :posts, inverse_of: :member
-
# accepts_nested_attributes_for :posts
-
# end
-
#
-
# class Post < ActiveRecord::Base
-
# belongs_to :member, inverse_of: :posts
-
# validates_presence_of :member
-
# end
-
#
-
# Note that if you do not specify the <tt>inverse_of</tt> option, then
-
# Active Record will try to automatically guess the inverse association
-
# based on heuristics.
-
#
-
# For one-to-one nested associations, if you build the new (in-memory)
-
# child object yourself before assignment, then this module will not
-
# overwrite it, e.g.:
-
#
-
# class Member < ActiveRecord::Base
-
# has_one :avatar
-
# accepts_nested_attributes_for :avatar
-
#
-
# def avatar
-
# super || build_avatar(width: 200)
-
# end
-
# end
-
#
-
# member = Member.new
-
# member.avatar_attributes = {icon: 'sad'}
-
# member.avatar.width # => 200
-
2
module ClassMethods
-
2
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
-
-
# Defines an attributes writer for the specified association(s).
-
#
-
# Supported options:
-
# [:allow_destroy]
-
# If true, destroys any members from the attributes hash with a
-
# <tt>_destroy</tt> key and a value that evaluates to +true+
-
# (eg. 1, '1', true, or 'true'). This option is off by default.
-
# [:reject_if]
-
# Allows you to specify a Proc or a Symbol pointing to a method
-
# that checks whether a record should be built for a certain attribute
-
# hash. The hash is passed to the supplied Proc or the method
-
# and it should return either +true+ or +false+. When no :reject_if
-
# is specified, a record will be built for all attribute hashes that
-
# do not have a <tt>_destroy</tt> value that evaluates to true.
-
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
-
# that will reject a record where all the attributes are blank excluding
-
# any value for _destroy.
-
# [:limit]
-
# Allows you to specify the maximum number of the associated records that
-
# can be processed with the nested attributes. Limit also can be specified as a
-
# Proc or a Symbol pointing to a method that should return number. If the size of the
-
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
-
# exception is raised. If omitted, any number associations can be processed.
-
# Note that the :limit option is only applicable to one-to-many associations.
-
# [:update_only]
-
# For a one-to-one association, this option allows you to specify how
-
# nested attributes are to be used when an associated record already
-
# exists. In general, an existing record may either be updated with the
-
# new set of attribute values or be replaced by a wholly new record
-
# containing those values. By default the :update_only option is +false+
-
# and the nested attributes are used to update the existing record only
-
# if they include the record's <tt>:id</tt> value. Otherwise a new
-
# record will be instantiated and used to replace the existing one.
-
# However if the :update_only option is +true+, the nested attributes
-
# are used to update the record's attributes always, regardless of
-
# whether the <tt>:id</tt> is present. The option is ignored for collection
-
# associations.
-
#
-
# Examples:
-
# # creates avatar_attributes=
-
# accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? }
-
# # creates avatar_attributes=
-
# accepts_nested_attributes_for :avatar, reject_if: :all_blank
-
# # creates avatar_attributes= and posts_attributes=
-
# accepts_nested_attributes_for :avatar, :posts, allow_destroy: true
-
2
def accepts_nested_attributes_for(*attr_names)
-
8
options = { :allow_destroy => false, :update_only => false }
-
8
options.update(attr_names.extract_options!)
-
8
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
-
8
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
-
-
8
attr_names.each do |association_name|
-
8
if reflection = _reflect_on_association(association_name)
-
8
reflection.autosave = true
-
8
define_autosave_validation_callbacks(reflection)
-
-
8
nested_attributes_options = self.nested_attributes_options.dup
-
8
nested_attributes_options[association_name.to_sym] = options
-
8
self.nested_attributes_options = nested_attributes_options
-
-
8
type = (reflection.collection? ? :collection : :one_to_one)
-
8
generate_association_writer(association_name, type)
-
else
-
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
-
end
-
end
-
end
-
-
2
private
-
-
# Generates a writer method for this association. Serves as a point for
-
# accessing the objects in the association. For example, this method
-
# could generate the following:
-
#
-
# def pirate_attributes=(attributes)
-
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
-
# end
-
#
-
# This redirects the attempts to write objects in an association through
-
# the helper methods defined below. Makes it seem like the nested
-
# associations are just regular associations.
-
2
def generate_association_writer(association_name, type)
-
8
generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
-
if method_defined?(:#{association_name}_attributes=)
-
remove_method(:#{association_name}_attributes=)
-
end
-
def #{association_name}_attributes=(attributes)
-
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
-
end
-
eoruby
-
end
-
end
-
-
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
-
# used in conjunction with fields_for to build a form element for the
-
# destruction of this association.
-
#
-
# See ActionView::Helpers::FormHelper::fields_for for more info.
-
2
def _destroy
-
marked_for_destruction?
-
end
-
-
2
private
-
-
# Attribute hash keys that should not be assigned as normal attributes.
-
# These hash keys are nested attributes implementation details.
-
2
UNASSIGNABLE_KEYS = %w( id _destroy )
-
-
# Assigns the given attributes to the association.
-
#
-
# If an associated record does not yet exist, one will be instantiated. If
-
# an associated record already exists, the method's behavior depends on
-
# the value of the update_only option. If update_only is +false+ and the
-
# given attributes include an <tt>:id</tt> that matches the existing record's
-
# id, then the existing record will be modified. If no <tt>:id</tt> is provided
-
# it will be replaced with a new record. If update_only is +true+ the existing
-
# record will be modified regardless of whether an <tt>:id</tt> is provided.
-
#
-
# If the given attributes include a matching <tt>:id</tt> attribute, or
-
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
-
# then the existing record will be marked for destruction.
-
2
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
-
options = self.nested_attributes_options[association_name]
-
attributes = attributes.with_indifferent_access
-
existing_record = send(association_name)
-
-
if (options[:update_only] || !attributes['id'].blank?) && existing_record &&
-
(options[:update_only] || existing_record.id.to_s == attributes['id'].to_s)
-
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
-
-
elsif attributes['id'].present?
-
raise_nested_attributes_record_not_found!(association_name, attributes['id'])
-
-
elsif !reject_new_record?(association_name, attributes)
-
assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
-
-
if existing_record && existing_record.new_record?
-
existing_record.assign_attributes(assignable_attributes)
-
association(association_name).initialize_attributes(existing_record)
-
else
-
method = "build_#{association_name}"
-
if respond_to?(method)
-
send(method, assignable_attributes)
-
else
-
raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
-
end
-
end
-
end
-
end
-
-
# Assigns the given attributes to the collection association.
-
#
-
# Hashes with an <tt>:id</tt> value matching an existing associated record
-
# will update that record. Hashes without an <tt>:id</tt> value will build
-
# a new record for the association. Hashes with a matching <tt>:id</tt>
-
# value and a <tt>:_destroy</tt> key set to a truthy value will mark the
-
# matched record for destruction.
-
#
-
# For example:
-
#
-
# assign_nested_attributes_for_collection_association(:people, {
-
# '1' => { id: '1', name: 'Peter' },
-
# '2' => { name: 'John' },
-
# '3' => { id: '2', _destroy: true }
-
# })
-
#
-
# Will update the name of the Person with ID 1, build a new associated
-
# person with the name 'John', and mark the associated Person with ID 2
-
# for destruction.
-
#
-
# Also accepts an Array of attribute hashes:
-
#
-
# assign_nested_attributes_for_collection_association(:people, [
-
# { id: '1', name: 'Peter' },
-
# { name: 'John' },
-
# { id: '2', _destroy: true }
-
# ])
-
2
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
-
options = self.nested_attributes_options[association_name]
-
-
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
-
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
-
end
-
-
check_record_limit!(options[:limit], attributes_collection)
-
-
if attributes_collection.is_a? Hash
-
keys = attributes_collection.keys
-
attributes_collection = if keys.include?('id') || keys.include?(:id)
-
[attributes_collection]
-
else
-
attributes_collection.values
-
end
-
end
-
-
association = association(association_name)
-
-
existing_records = if association.loaded?
-
association.target
-
else
-
attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
-
attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
-
end
-
-
attributes_collection.each do |attributes|
-
attributes = attributes.with_indifferent_access
-
-
if attributes['id'].blank?
-
unless reject_new_record?(association_name, attributes)
-
association.build(attributes.except(*UNASSIGNABLE_KEYS))
-
end
-
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
-
unless call_reject_if(association_name, attributes)
-
# Make sure we are operating on the actual object which is in the association's
-
# proxy_target array (either by finding it, or adding it if not found)
-
# Take into account that the proxy_target may have changed due to callbacks
-
target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
-
if target_record
-
existing_record = target_record
-
else
-
association.add_to_target(existing_record, :skip_callbacks)
-
end
-
-
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
-
end
-
else
-
raise_nested_attributes_record_not_found!(association_name, attributes['id'])
-
end
-
end
-
end
-
-
# Takes in a limit and checks if the attributes_collection has too many
-
# records. It accepts limit in the form of symbol, proc, or
-
# number-like object (anything that can be compared with an integer).
-
#
-
# Raises TooManyRecords error if the attributes_collection is
-
# larger than the limit.
-
2
def check_record_limit!(limit, attributes_collection)
-
if limit
-
limit = case limit
-
when Symbol
-
send(limit)
-
when Proc
-
limit.call
-
else
-
limit
-
end
-
-
if limit && attributes_collection.size > limit
-
raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
-
end
-
end
-
end
-
-
# Updates a record with the +attributes+ or marks it for destruction if
-
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
-
2
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
-
record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
-
record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
-
end
-
-
# Determines if a hash contains a truthy _destroy key.
-
2
def has_destroy_flag?(hash)
-
Type::Boolean.new.type_cast_from_user(hash['_destroy'])
-
end
-
-
# Determines if a new record should be rejected by checking
-
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
-
# association and evaluates to +true+.
-
2
def reject_new_record?(association_name, attributes)
-
will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes)
-
end
-
-
# Determines if a record with the particular +attributes+ should be
-
# rejected by calling the reject_if Symbol or Proc (if defined).
-
# The reject_if option is defined by +accepts_nested_attributes_for+.
-
#
-
# Returns false if there is a +destroy_flag+ on the attributes.
-
2
def call_reject_if(association_name, attributes)
-
return false if will_be_destroyed?(association_name, attributes)
-
-
case callback = self.nested_attributes_options[association_name][:reject_if]
-
when Symbol
-
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
-
when Proc
-
callback.call(attributes)
-
end
-
end
-
-
# Only take into account the destroy flag if <tt>:allow_destroy</tt> is true
-
2
def will_be_destroyed?(association_name, attributes)
-
allow_destroy?(association_name) && has_destroy_flag?(attributes)
-
end
-
-
2
def allow_destroy?(association_name)
-
self.nested_attributes_options[association_name][:allow_destroy]
-
end
-
-
2
def raise_nested_attributes_record_not_found!(association_name, record_id)
-
raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record No Touching
-
2
module NoTouching
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
# Lets you selectively disable calls to `touch` for the
-
# duration of a block.
-
#
-
# ==== Examples
-
# ActiveRecord::Base.no_touching do
-
# Project.first.touch # does nothing
-
# Message.first.touch # does nothing
-
# end
-
#
-
# Project.no_touching do
-
# Project.first.touch # does nothing
-
# Message.first.touch # works, but does not touch the associated project
-
# end
-
#
-
2
def no_touching(&block)
-
NoTouching.apply_to(self, &block)
-
end
-
end
-
-
2
class << self
-
2
def apply_to(klass) #:nodoc:
-
klasses.push(klass)
-
yield
-
ensure
-
klasses.pop
-
end
-
-
2
def applied_to?(klass) #:nodoc:
-
klasses.any? { |k| k >= klass }
-
end
-
-
2
private
-
2
def klasses
-
Thread.current[:no_touching_classes] ||= []
-
end
-
end
-
-
2
def no_touching?
-
NoTouching.applied_to?(self.class)
-
end
-
-
2
def touch(*) # :nodoc:
-
super unless no_touching?
-
end
-
end
-
end
-
# -*- coding: utf-8 -*-
-
-
1
module ActiveRecord
-
1
module NullRelation # :nodoc:
-
1
def exec_queries
-
@records = []
-
end
-
-
1
def pluck(*column_names)
-
[]
-
end
-
-
1
def delete_all(_conditions = nil)
-
0
-
end
-
-
1
def update_all(_updates, _conditions = nil, _options = {})
-
0
-
end
-
-
1
def delete(_id_or_array)
-
0
-
end
-
-
1
def size
-
calculate :size, nil
-
end
-
-
1
def empty?
-
true
-
end
-
-
1
def any?
-
false
-
end
-
-
1
def many?
-
false
-
end
-
-
1
def to_sql
-
""
-
end
-
-
1
def count(*)
-
calculate :count, nil
-
end
-
-
1
def sum(*)
-
calculate :sum, nil
-
end
-
-
1
def average(*)
-
calculate :average, nil
-
end
-
-
1
def minimum(*)
-
calculate :minimum, nil
-
end
-
-
1
def maximum(*)
-
calculate :maximum, nil
-
end
-
-
1
def calculate(operation, _column_name, _options = {})
-
# TODO: Remove _options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
if [:count, :sum, :size].include? operation
-
group_values.any? ? Hash.new : 0
-
elsif [:average, :minimum, :maximum].include?(operation) && group_values.any?
-
Hash.new
-
else
-
nil
-
end
-
end
-
-
1
def exists?(_id = false)
-
false
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record Persistence
-
2
module Persistence
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
-
# The resulting object is returned whether the object was saved successfully to the database or not.
-
#
-
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
-
# attributes on the objects that are to be created.
-
#
-
# ==== Examples
-
# # Create a single new object
-
# User.create(first_name: 'Jamie')
-
#
-
# # Create an Array of new objects
-
# User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }])
-
#
-
# # Create a single object and pass it into a block to set other attributes.
-
# User.create(first_name: 'Jamie') do |u|
-
# u.is_admin = false
-
# end
-
#
-
# # Creating an Array of new objects using a block, where the block is executed for each object:
-
# User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }]) do |u|
-
# u.is_admin = false
-
# end
-
2
def create(attributes = nil, &block)
-
if attributes.is_a?(Array)
-
attributes.collect { |attr| create(attr, &block) }
-
else
-
object = new(attributes, &block)
-
object.save
-
object
-
end
-
end
-
-
# Creates an object (or multiple objects) and saves it to the database,
-
# if validations pass. Raises a RecordInvalid error if validations fail,
-
# unlike Base#create.
-
#
-
# The +attributes+ parameter can be either a Hash or an Array of Hashes.
-
# These describe which attributes to be created on the object, or
-
# multiple objects when given an Array of Hashes.
-
2
def create!(attributes = nil, &block)
-
if attributes.is_a?(Array)
-
attributes.collect { |attr| create!(attr, &block) }
-
else
-
object = new(attributes, &block)
-
object.save!
-
object
-
end
-
end
-
-
# Given an attributes hash, +instantiate+ returns a new instance of
-
# the appropriate class. Accepts only keys as strings.
-
#
-
# For example, +Post.all+ may return Comments, Messages, and Emails
-
# by storing the record's subclass in a +type+ attribute. By calling
-
# +instantiate+ instead of +new+, finder methods ensure they get new
-
# instances of the appropriate class for each record.
-
#
-
# See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see
-
# how this "single-table" inheritance mapping is implemented.
-
2
def instantiate(attributes, column_types = {})
-
162
klass = discriminate_class_for_record(attributes)
-
162
attributes = klass.attributes_builder.build_from_database(attributes, column_types)
-
162
klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
-
end
-
-
2
private
-
# Called by +instantiate+ to decide which class to use for a new
-
# record instance.
-
#
-
# See +ActiveRecord::Inheritance#discriminate_class_for_record+ for
-
# the single-table inheritance discriminator.
-
2
def discriminate_class_for_record(record)
-
162
self
-
end
-
end
-
-
# Returns true if this object hasn't been saved yet -- that is, a record
-
# for the object doesn't exist in the database yet; otherwise, returns false.
-
2
def new_record?
-
110
sync_with_transaction_state
-
110
@new_record
-
end
-
-
# Returns true if this object has been destroyed, otherwise returns false.
-
2
def destroyed?
-
47
sync_with_transaction_state
-
47
@destroyed
-
end
-
-
# Returns true if the record is persisted, i.e. it's not a new record and it was
-
# not destroyed, otherwise returns false.
-
2
def persisted?
-
69
!(new_record? || destroyed?)
-
end
-
-
# Saves the model.
-
#
-
# If the model is new a record gets created in the database, otherwise
-
# the existing record gets updated.
-
#
-
# By default, save always run validations. If any of them fail the action
-
# is cancelled and +save+ returns +false+. However, if you supply
-
# validate: false, validations are bypassed altogether. See
-
# ActiveRecord::Validations for more information.
-
#
-
# There's a series of callbacks associated with +save+. If any of the
-
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
-
# +save+ returns +false+. See ActiveRecord::Callbacks for further
-
# details.
-
#
-
# Attributes marked as readonly are silently ignored if the record is
-
# being updated.
-
2
def save(*)
-
10
create_or_update
-
rescue ActiveRecord::RecordInvalid
-
false
-
end
-
-
# Saves the model.
-
#
-
# If the model is new a record gets created in the database, otherwise
-
# the existing record gets updated.
-
#
-
# With <tt>save!</tt> validations always run. If any of them fail
-
# ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations
-
# for more information.
-
#
-
# There's a series of callbacks associated with <tt>save!</tt>. If any of
-
# the <tt>before_*</tt> callbacks return +false+ the action is cancelled
-
# and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
-
# ActiveRecord::Callbacks for further details.
-
#
-
# Attributes marked as readonly are silently ignored if the record is
-
# being updated.
-
2
def save!(*)
-
create_or_update || raise(RecordNotSaved.new("Failed to save the record", self))
-
end
-
-
# Deletes the record in the database and freezes this instance to
-
# reflect that no changes should be made (since they can't be
-
# persisted). Returns the frozen instance.
-
#
-
# The row is simply removed with an SQL +DELETE+ statement on the
-
# record's primary key, and no callbacks are executed.
-
#
-
# To enforce the object's +before_destroy+ and +after_destroy+
-
# callbacks or any <tt>:dependent</tt> association
-
# options, use <tt>#destroy</tt>.
-
2
def delete
-
self.class.delete(id) if persisted?
-
@destroyed = true
-
freeze
-
end
-
-
# Deletes the record in the database and freezes this instance to reflect
-
# that no changes should be made (since they can't be persisted).
-
#
-
# There's a series of callbacks associated with <tt>destroy</tt>. If
-
# the <tt>before_destroy</tt> callback return +false+ the action is cancelled
-
# and <tt>destroy</tt> returns +false+. See
-
# ActiveRecord::Callbacks for further details.
-
2
def destroy
-
3
raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
-
3
destroy_associations
-
3
self.class.connection.add_transaction_record(self)
-
3
destroy_row if persisted?
-
3
@destroyed = true
-
3
freeze
-
end
-
-
# Deletes the record in the database and freezes this instance to reflect
-
# that no changes should be made (since they can't be persisted).
-
#
-
# There's a series of callbacks associated with <tt>destroy!</tt>. If
-
# the <tt>before_destroy</tt> callback return +false+ the action is cancelled
-
# and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See
-
# ActiveRecord::Callbacks for further details.
-
2
def destroy!
-
destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self))
-
end
-
-
# Returns an instance of the specified +klass+ with the attributes of the
-
# current record. This is mostly useful in relation to single-table
-
# inheritance structures where you want a subclass to appear as the
-
# superclass. This can be used along with record identification in
-
# Action Pack to allow, say, <tt>Client < Company</tt> to do something
-
# like render <tt>partial: @client.becomes(Company)</tt> to render that
-
# instance using the companies/company partial instead of clients/client.
-
#
-
# Note: The new instance will share a link to the same attributes as the original class.
-
# So any change to the attributes in either instance will affect the other.
-
2
def becomes(klass)
-
became = klass.new
-
became.instance_variable_set("@attributes", @attributes)
-
changed_attributes = @changed_attributes if defined?(@changed_attributes)
-
became.instance_variable_set("@changed_attributes", changed_attributes || {})
-
became.instance_variable_set("@new_record", new_record?)
-
became.instance_variable_set("@destroyed", destroyed?)
-
became.instance_variable_set("@errors", errors)
-
became
-
end
-
-
# Wrapper around +becomes+ that also changes the instance's sti column value.
-
# This is especially useful if you want to persist the changed class in your
-
# database.
-
#
-
# Note: The old instance's sti column value will be changed too, as both objects
-
# share the same set of attributes.
-
2
def becomes!(klass)
-
became = becomes(klass)
-
sti_type = nil
-
if !klass.descends_from_active_record?
-
sti_type = klass.sti_name
-
end
-
became.public_send("#{klass.inheritance_column}=", sti_type)
-
became
-
end
-
-
# Updates a single attribute and saves the record.
-
# This is especially useful for boolean flags on existing records. Also note that
-
#
-
# * Validation is skipped.
-
# * Callbacks are invoked.
-
# * updated_at/updated_on column is updated if that column is available.
-
# * Updates all the attributes that are dirty in this object.
-
#
-
# This method raises an +ActiveRecord::ActiveRecordError+ if the
-
# attribute is marked as readonly.
-
#
-
# See also +update_column+.
-
2
def update_attribute(name, value)
-
name = name.to_s
-
verify_readonly_attribute(name)
-
send("#{name}=", value)
-
save(validate: false)
-
end
-
-
# Updates the attributes of the model from the passed-in hash and saves the
-
# record, all wrapped in a transaction. If the object is invalid, the saving
-
# will fail and false will be returned.
-
2
def update(attributes)
-
# The following transaction covers any possible database side-effects of the
-
# attributes assignment. For example, setting the IDs of a child collection.
-
6
with_transaction_returning_status do
-
6
assign_attributes(attributes)
-
6
save
-
end
-
end
-
-
2
alias update_attributes update
-
-
# Updates its receiver just like +update+ but calls <tt>save!</tt> instead
-
# of +save+, so an exception is raised if the record is invalid.
-
2
def update!(attributes)
-
# The following transaction covers any possible database side-effects of the
-
# attributes assignment. For example, setting the IDs of a child collection.
-
with_transaction_returning_status do
-
assign_attributes(attributes)
-
save!
-
end
-
end
-
-
2
alias update_attributes! update!
-
-
# Equivalent to <code>update_columns(name => value)</code>.
-
2
def update_column(name, value)
-
update_columns(name => value)
-
end
-
-
# Updates the attributes directly in the database issuing an UPDATE SQL
-
# statement and sets them in the receiver:
-
#
-
# user.update_columns(last_request_at: Time.current)
-
#
-
# This is the fastest way to update attributes because it goes straight to
-
# the database, but take into account that in consequence the regular update
-
# procedures are totally bypassed. In particular:
-
#
-
# * Validations are skipped.
-
# * Callbacks are skipped.
-
# * +updated_at+/+updated_on+ are not updated.
-
#
-
# This method raises an +ActiveRecord::ActiveRecordError+ when called on new
-
# objects, or when at least one of the attributes is marked as readonly.
-
2
def update_columns(attributes)
-
raise ActiveRecordError, "cannot update a new record" if new_record?
-
raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
-
-
attributes.each_key do |key|
-
verify_readonly_attribute(key.to_s)
-
end
-
-
updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes)
-
-
attributes.each do |k, v|
-
raw_write_attribute(k, v)
-
end
-
-
updated_count == 1
-
end
-
-
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
-
# The increment is performed directly on the underlying attribute, no setter is invoked.
-
# Only makes sense for number-based attributes. Returns +self+.
-
2
def increment(attribute, by = 1)
-
self[attribute] ||= 0
-
self[attribute] += by
-
self
-
end
-
-
# Wrapper around +increment+ that saves the record. This method differs from
-
# its non-bang version in that it passes through the attribute setter.
-
# Saving is not subjected to validation checks. Returns +true+ if the
-
# record could be saved.
-
2
def increment!(attribute, by = 1)
-
increment(attribute, by).update_attribute(attribute, self[attribute])
-
end
-
-
# Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1).
-
# The decrement is performed directly on the underlying attribute, no setter is invoked.
-
# Only makes sense for number-based attributes. Returns +self+.
-
2
def decrement(attribute, by = 1)
-
self[attribute] ||= 0
-
self[attribute] -= by
-
self
-
end
-
-
# Wrapper around +decrement+ that saves the record. This method differs from
-
# its non-bang version in that it passes through the attribute setter.
-
# Saving is not subjected to validation checks. Returns +true+ if the
-
# record could be saved.
-
2
def decrement!(attribute, by = 1)
-
decrement(attribute, by).update_attribute(attribute, self[attribute])
-
end
-
-
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
-
# if the predicate returns +true+ the attribute will become +false+. This
-
# method toggles directly the underlying value without calling any setter.
-
# Returns +self+.
-
2
def toggle(attribute)
-
self[attribute] = !send("#{attribute}?")
-
self
-
end
-
-
# Wrapper around +toggle+ that saves the record. This method differs from
-
# its non-bang version in that it passes through the attribute setter.
-
# Saving is not subjected to validation checks. Returns +true+ if the
-
# record could be saved.
-
2
def toggle!(attribute)
-
toggle(attribute).update_attribute(attribute, self[attribute])
-
end
-
-
# Reloads the record from the database.
-
#
-
# This method finds record by its primary key (which could be assigned manually) and
-
# modifies the receiver in-place:
-
#
-
# account = Account.new
-
# # => #<Account id: nil, email: nil>
-
# account.id = 1
-
# account.reload
-
# # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]]
-
# # => #<Account id: 1, email: 'account@example.com'>
-
#
-
# Attributes are reloaded from the database, and caches busted, in
-
# particular the associations cache and the QueryCache.
-
#
-
# If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
-
# is raised. Otherwise, in addition to the in-place modification the method
-
# returns +self+ for convenience.
-
#
-
# The optional <tt>:lock</tt> flag option allows you to lock the reloaded record:
-
#
-
# reload(lock: true) # reload with pessimistic locking
-
#
-
# Reloading is commonly used in test suites to test something is actually
-
# written to the database, or when some action modifies the corresponding
-
# row in the database but not the object in memory:
-
#
-
# assert account.deposit!(25)
-
# assert_equal 25, account.credit # check it is updated in memory
-
# assert_equal 25, account.reload.credit # check it is also persisted
-
#
-
# Another common use case is optimistic locking handling:
-
#
-
# def with_optimistic_retry
-
# begin
-
# yield
-
# rescue ActiveRecord::StaleObjectError
-
# begin
-
# # Reload lock_version in particular.
-
# reload
-
# rescue ActiveRecord::RecordNotFound
-
# # If the record is gone there is nothing to do.
-
# else
-
# retry
-
# end
-
# end
-
# end
-
#
-
2
def reload(options = nil)
-
clear_aggregation_cache
-
clear_association_cache
-
self.class.connection.clear_query_cache
-
-
fresh_object =
-
if options && options[:lock]
-
self.class.unscoped { self.class.lock(options[:lock]).find(id) }
-
else
-
self.class.unscoped { self.class.find(id) }
-
end
-
-
@attributes = fresh_object.instance_variable_get('@attributes')
-
@new_record = false
-
self
-
end
-
-
# Saves the record with the updated_at/on attributes set to the current time.
-
# Please note that no validation is performed and only the +after_touch+,
-
# +after_commit+ and +after_rollback+ callbacks are executed.
-
#
-
# If attribute names are passed, they are updated along with updated_at/on
-
# attributes.
-
#
-
# product.touch # updates updated_at/on
-
# product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
-
# product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes
-
#
-
# If used along with +belongs_to+ then +touch+ will invoke +touch+ method on
-
# associated object.
-
#
-
# class Brake < ActiveRecord::Base
-
# belongs_to :car, touch: true
-
# end
-
#
-
# class Car < ActiveRecord::Base
-
# belongs_to :corporation, touch: true
-
# end
-
#
-
# # triggers @brake.car.touch and @brake.car.corporation.touch
-
# @brake.touch
-
#
-
# Note that +touch+ must be used on a persisted object, or else an
-
# ActiveRecordError will be thrown. For example:
-
#
-
# ball = Ball.new
-
# ball.touch(:updated_at) # => raises ActiveRecordError
-
#
-
2
def touch(*names)
-
raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
-
-
attributes = timestamp_attributes_for_update_in_model
-
attributes.concat(names)
-
-
unless attributes.empty?
-
current_time = current_time_from_proper_timezone
-
changes = {}
-
-
attributes.each do |column|
-
column = column.to_s
-
changes[column] = write_attribute(column, current_time)
-
end
-
-
changes[self.class.locking_column] = increment_lock if locking_enabled?
-
-
clear_attribute_changes(changes.keys)
-
primary_key = self.class.primary_key
-
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
-
else
-
true
-
end
-
end
-
-
2
private
-
-
# A hook to be overridden by association modules.
-
2
def destroy_associations
-
end
-
-
2
def destroy_row
-
3
relation_for_destroy.delete_all
-
end
-
-
2
def relation_for_destroy
-
3
pk = self.class.primary_key
-
3
column = self.class.columns_hash[pk]
-
3
substitute = self.class.connection.substitute_at(column)
-
-
3
relation = self.class.unscoped.where(
-
self.class.arel_table[pk].eq(substitute))
-
-
3
relation.bind_values = [[column, id]]
-
3
relation
-
end
-
-
2
def create_or_update
-
10
raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
-
10
result = new_record? ? _create_record : _update_record
-
10
result != false
-
end
-
-
# Updates the associated record with values matching those of the instance attributes.
-
# Returns the number of affected rows.
-
2
def _update_record(attribute_names = self.attribute_names)
-
4
attributes_values = arel_attributes_with_values_for_update(attribute_names)
-
4
if attributes_values.empty?
-
2
0
-
else
-
2
self.class.unscoped._update_record attributes_values, id, id_was
-
end
-
end
-
-
# Creates a record with values matching those of the instance attributes
-
# and returns its id.
-
2
def _create_record(attribute_names = self.attribute_names)
-
6
attributes_values = arel_attributes_with_values_for_create(attribute_names)
-
-
6
new_id = self.class.unscoped.insert attributes_values
-
6
self.id ||= new_id if self.class.primary_key
-
-
6
@new_record = false
-
6
id
-
end
-
-
2
def verify_readonly_attribute(name)
-
raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record Query Cache
-
2
class QueryCache
-
2
module ClassMethods
-
# Enable the query cache within the block if Active Record is configured.
-
# If it's not, it will execute the given block.
-
2
def cache(&block)
-
if ActiveRecord::Base.connected?
-
connection.cache(&block)
-
else
-
yield
-
end
-
end
-
-
# Disable the query cache within the block if Active Record is configured.
-
# If it's not, it will execute the given block.
-
2
def uncached(&block)
-
if ActiveRecord::Base.connected?
-
connection.uncached(&block)
-
else
-
yield
-
end
-
end
-
end
-
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
connection = ActiveRecord::Base.connection
-
enabled = connection.query_cache_enabled
-
connection_id = ActiveRecord::Base.connection_id
-
connection.enable_query_cache!
-
-
response = @app.call(env)
-
response[2] = Rack::BodyProxy.new(response[2]) do
-
restore_query_cache_settings(connection_id, enabled)
-
end
-
-
response
-
rescue Exception => e
-
restore_query_cache_settings(connection_id, enabled)
-
raise e
-
end
-
-
2
private
-
-
2
def restore_query_cache_settings(connection_id, enabled)
-
ActiveRecord::Base.connection_id = connection_id
-
ActiveRecord::Base.connection.clear_query_cache
-
ActiveRecord::Base.connection.disable_query_cache! unless enabled
-
end
-
-
end
-
end
-
2
module ActiveRecord
-
2
module Querying
-
2
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
-
2
delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all
-
2
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
-
2
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
-
2
delegate :find_by, :find_by!, to: :all
-
2
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
-
2
delegate :find_each, :find_in_batches, to: :all
-
2
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
-
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
-
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
-
2
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
-
2
delegate :pluck, :ids, to: :all
-
-
# Executes a custom SQL query against your database and returns all the results. The results will
-
# be returned as an array with columns requested encapsulated as attributes of the model you call
-
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
-
# a +Product+ object with the attributes you specified in the SQL query.
-
#
-
# If you call a complicated SQL query which spans multiple tables the columns specified by the
-
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
-
# table.
-
#
-
# The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
-
# no database agnostic conversions performed. This should be a last resort because using, for example,
-
# MySQL specific terms will lock you to using that particular database engine or require you to
-
# change your call if you switch engines.
-
#
-
# # A simple SQL query spanning multiple tables
-
# Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
-
# # => [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
-
#
-
# You can use the same string replacement techniques as you can with <tt>ActiveRecord::QueryMethods#where</tt>:
-
#
-
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
-
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
-
2
def find_by_sql(sql, binds = [])
-
118
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
-
118
column_types = result_set.column_types.dup
-
1452
columns_hash.each_key { |k| column_types.delete k }
-
118
message_bus = ActiveSupport::Notifications.instrumenter
-
-
118
payload = {
-
record_count: result_set.length,
-
class_name: name
-
}
-
-
118
message_bus.instrument('instantiation.active_record', payload) do
-
280
result_set.map { |record| instantiate(record, column_types) }
-
end
-
end
-
-
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
-
# The use of this method should be restricted to complicated SQL queries that can't be executed
-
# using the ActiveRecord::Calculations class methods. Look into those before using this.
-
#
-
# ==== Parameters
-
#
-
# * +sql+ - An SQL statement which should return a count query from the database, see the example below.
-
#
-
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
-
2
def count_by_sql(sql)
-
sql = sanitize_conditions(sql)
-
connection.select_value(sql, "#{name} Count").to_i
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/attr_internal'
-
2
require 'active_record/log_subscriber'
-
-
2
module ActiveRecord
-
2
module Railties # :nodoc:
-
2
module ControllerRuntime #:nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
protected
-
-
2
attr_internal :db_runtime
-
-
2
def process_action(action, *args)
-
# We also need to reset the runtime before each action
-
# because of queries in middleware or in cases we are streaming
-
# and it won't be cleaned up by the method below.
-
29
ActiveRecord::LogSubscriber.reset_runtime
-
29
super
-
end
-
-
2
def cleanup_view_runtime
-
16
if ActiveRecord::Base.connected?
-
16
db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
-
16
self.db_runtime = (db_runtime || 0) + db_rt_before_render
-
16
runtime = super
-
16
db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
-
16
self.db_runtime += db_rt_after_render
-
16
runtime - db_rt_after_render
-
else
-
super
-
end
-
end
-
-
2
def append_info_to_payload(payload)
-
29
super
-
29
if ActiveRecord::Base.connected?
-
29
payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
-
end
-
end
-
-
2
module ClassMethods # :nodoc:
-
2
def log_process_action(payload)
-
29
messages, db_runtime = super, payload[:db_runtime]
-
29
messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
-
29
messages
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module ReadonlyAttributes
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :_attr_readonly, instance_accessor: false
-
2
self._attr_readonly = []
-
end
-
-
2
module ClassMethods
-
# Attributes listed as readonly will be used to create a new record but update operations will
-
# ignore these fields.
-
2
def attr_readonly(*attributes)
-
self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
-
end
-
-
# Returns an array of all the attributes that have been specified as readonly.
-
2
def readonly_attributes
-
5
self._attr_readonly
-
end
-
end
-
end
-
end
-
2
require 'thread'
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
# = Active Record Reflection
-
2
module Reflection # :nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :_reflections, instance_writer: false
-
2
class_attribute :aggregate_reflections, instance_writer: false
-
2
self._reflections = {}
-
2
self.aggregate_reflections = {}
-
end
-
-
2
def self.create(macro, name, scope, options, ar)
-
24
klass = case macro
-
when :composed_of
-
AggregateReflection
-
when :has_many
-
16
HasManyReflection
-
when :has_one
-
HasOneReflection
-
when :belongs_to
-
8
BelongsToReflection
-
else
-
raise "Unsupported Macro: #{macro}"
-
end
-
-
24
reflection = klass.new(name, scope, options, ar)
-
24
options[:through] ? ThroughReflection.new(reflection) : reflection
-
end
-
-
2
def self.add_reflection(ar, name, reflection)
-
24
ar.clear_reflections_cache
-
24
ar._reflections = ar._reflections.merge(name.to_s => reflection)
-
end
-
-
2
def self.add_aggregate_reflection(ar, name, reflection)
-
ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
-
end
-
-
# \Reflection enables interrogating of Active Record classes and objects
-
# about their associations and aggregations. This information can,
-
# for example, be used in a form builder that takes an Active Record object
-
# and creates input fields for all of the attributes depending on their type
-
# and displays the associations to other objects.
-
#
-
# MacroReflection class has info for AggregateReflection and AssociationReflection
-
# classes.
-
2
module ClassMethods
-
# Returns an array of AggregateReflection objects for all the aggregations in the class.
-
2
def reflect_on_all_aggregations
-
4
aggregate_reflections.values
-
end
-
-
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
-
#
-
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
-
#
-
2
def reflect_on_aggregation(aggregation)
-
200
aggregate_reflections[aggregation.to_s]
-
end
-
-
# Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
-
#
-
# Account.reflections # => {"balance" => AggregateReflection}
-
#
-
# @api public
-
2
def reflections
-
@__reflections ||= begin
-
ref = {}
-
-
_reflections.each do |name, reflection|
-
parent_name, parent_reflection = reflection.parent_reflection
-
-
if parent_name
-
ref[parent_name] = parent_reflection
-
else
-
ref[name] = reflection
-
end
-
end
-
-
ref
-
end
-
end
-
-
# Returns an array of AssociationReflection objects for all the
-
# associations in the class. If you only want to reflect on a certain
-
# association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
-
# <tt>:belongs_to</tt>) as the first parameter.
-
#
-
# Example:
-
#
-
# Account.reflect_on_all_associations # returns an array of all associations
-
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
-
#
-
# @api public
-
2
def reflect_on_all_associations(macro = nil)
-
association_reflections = reflections.values
-
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
-
end
-
-
# Returns the AssociationReflection object for the +association+ (use the symbol).
-
#
-
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
-
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
-
#
-
# @api public
-
2
def reflect_on_association(association)
-
reflections[association.to_s]
-
end
-
-
# @api private
-
2
def _reflect_on_association(association) #:nodoc:
-
300
_reflections[association.to_s]
-
end
-
-
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
-
#
-
# @api public
-
2
def reflect_on_all_autosave_associations
-
reflections.values.select { |reflection| reflection.options[:autosave] }
-
end
-
-
2
def clear_reflections_cache #:nodoc:
-
24
@__reflections = nil
-
end
-
end
-
-
# Holds all the methods that are shared between MacroReflection, AssociationReflection
-
# and ThroughReflection
-
2
class AbstractReflection # :nodoc:
-
2
def table_name
-
18
klass.table_name
-
end
-
-
# Returns a new, unsaved instance of the associated class. +attributes+ will
-
# be passed to the class's constructor.
-
2
def build_association(attributes, &block)
-
klass.new(attributes, &block)
-
end
-
-
2
def quoted_table_name
-
klass.quoted_table_name
-
end
-
-
2
def primary_key_type
-
klass.type_for_attribute(klass.primary_key)
-
end
-
-
# Returns the class name for the macro.
-
#
-
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
-
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
-
2
def class_name
-
26
@class_name ||= (options[:class_name] || derive_class_name).to_s
-
end
-
-
2
JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
-
-
2
def join_keys(assoc_klass)
-
3
JoinKeys.new(foreign_key, active_record_primary_key)
-
end
-
-
2
def source_macro
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
ActiveRecord::Base.source_macro is deprecated and will be removed
-
without replacement.
-
MSG
-
-
macro
-
end
-
-
2
def inverse_of
-
4
return unless inverse_name
-
-
3
@inverse_of ||= klass._reflect_on_association inverse_name
-
end
-
-
2
def check_validity_of_inverse!
-
7
unless polymorphic?
-
7
if has_inverse? && inverse_of.nil?
-
raise InverseOfAssociationNotFoundError.new(self)
-
end
-
end
-
end
-
end
-
# Base class for AggregateReflection and AssociationReflection. Objects of
-
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
-
#
-
# MacroReflection
-
# AssociationReflection
-
# AggregateReflection
-
# HasManyReflection
-
# HasOneReflection
-
# BelongsToReflection
-
# ThroughReflection
-
2
class MacroReflection < AbstractReflection
-
# Returns the name of the macro.
-
#
-
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
-
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
-
2
attr_reader :name
-
-
2
attr_reader :scope
-
-
# Returns the hash of options used for the macro.
-
#
-
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
-
# <tt>has_many :clients</tt> returns <tt>{}</tt>
-
2
attr_reader :options
-
-
2
attr_reader :active_record
-
-
2
attr_reader :plural_name # :nodoc:
-
-
2
def initialize(name, scope, options, active_record)
-
24
@name = name
-
24
@scope = scope
-
24
@options = options
-
24
@active_record = active_record
-
24
@klass = options[:anonymous_class]
-
24
@plural_name = active_record.pluralize_table_names ?
-
name.to_s.pluralize : name.to_s
-
end
-
-
2
def autosave=(autosave)
-
8
@automatic_inverse_of = false
-
8
@options[:autosave] = autosave
-
8
_, parent_reflection = self.parent_reflection
-
8
if parent_reflection
-
parent_reflection.autosave = autosave
-
end
-
end
-
-
# Returns the class for the macro.
-
#
-
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
-
# <tt>has_many :clients</tt> returns the Client class
-
2
def klass
-
@klass ||= compute_class(class_name)
-
end
-
-
2
def compute_class(name)
-
name.constantize
-
end
-
-
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
-
# and +other_aggregation+ has an options hash assigned to it.
-
2
def ==(other_aggregation)
-
super ||
-
other_aggregation.kind_of?(self.class) &&
-
name == other_aggregation.name &&
-
!other_aggregation.options.nil? &&
-
24
active_record == other_aggregation.active_record
-
end
-
-
2
private
-
2
def derive_class_name
-
name.to_s.camelize
-
end
-
end
-
-
-
# Holds all the meta-data about an aggregation as it was specified in the
-
# Active Record class.
-
2
class AggregateReflection < MacroReflection #:nodoc:
-
2
def mapping
-
mapping = options[:mapping] || [name, name]
-
mapping.first.is_a?(Array) ? mapping : [mapping]
-
end
-
end
-
-
# Holds all the meta-data about an association as it was specified in the
-
# Active Record class.
-
2
class AssociationReflection < MacroReflection #:nodoc:
-
# Returns the target association's class.
-
#
-
# class Author < ActiveRecord::Base
-
# has_many :books
-
# end
-
#
-
# Author.reflect_on_association(:books).klass
-
# # => Book
-
#
-
# <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
-
# a new association object. Use +build_association+ or +create_association+
-
# instead. This allows plugins to hook into association object creation.
-
2
def klass
-
146
@klass ||= compute_class(class_name)
-
end
-
-
2
def compute_class(name)
-
18
active_record.send(:compute_type, name)
-
end
-
-
2
attr_reader :type, :foreign_type
-
2
attr_accessor :parent_reflection # [:name, Reflection]
-
-
2
def initialize(name, scope, options, active_record)
-
24
super
-
24
@automatic_inverse_of = nil
-
24
@type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
-
24
@foreign_type = options[:foreign_type] || "#{name}_type"
-
24
@constructable = calculate_constructable(macro, options)
-
24
@association_scope_cache = {}
-
24
@scope_lock = Mutex.new
-
end
-
-
2
def association_scope_cache(conn, owner)
-
3
key = conn.prepared_statements
-
3
if polymorphic?
-
key = [key, owner._read_attribute(@foreign_type)]
-
end
-
3
@association_scope_cache[key] ||= @scope_lock.synchronize {
-
2
@association_scope_cache[key] ||= yield
-
}
-
end
-
-
2
def constructable? # :nodoc:
-
8
@constructable
-
end
-
-
2
def join_table
-
@join_table ||= options[:join_table] || derive_join_table
-
end
-
-
2
def foreign_key
-
55
@foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze
-
end
-
-
2
def association_foreign_key
-
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
-
end
-
-
# klass option is necessary to support loading polymorphic associations
-
2
def association_primary_key(klass = nil)
-
5
options[:primary_key] || primary_key(klass || self.klass)
-
end
-
-
2
def active_record_primary_key
-
3
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
-
end
-
-
2
def counter_cache_column
-
10
if options[:counter_cache] == true
-
"#{active_record.name.demodulize.underscore.pluralize}_count"
-
10
elsif options[:counter_cache]
-
options[:counter_cache].to_s
-
end
-
end
-
-
2
def check_validity!
-
7
check_validity_of_inverse!
-
end
-
-
2
def check_preloadable!
-
return unless scope
-
-
if scope.arity > 0
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
The association scope '#{name}' is instance dependent (the scope
-
block takes an argument). Preloading happens before the individual
-
instances are created. This means that there is no instance being
-
passed to the association scope. This will most likely result in
-
broken or incorrect behavior. Joining, Preloading and eager loading
-
of these associations is deprecated and will be removed in the future.
-
MSG
-
end
-
end
-
2
alias :check_eager_loadable! :check_preloadable!
-
-
2
def join_id_for(owner) # :nodoc:
-
owner[active_record_primary_key]
-
end
-
-
2
def through_reflection
-
nil
-
end
-
-
2
def source_reflection
-
3
self
-
end
-
-
# A chain of reflections from this one back to the owner. For more see the explanation in
-
# ThroughReflection.
-
2
def chain
-
11
[self]
-
end
-
-
2
def nested?
-
false
-
end
-
-
# An array of arrays of scopes. Each item in the outside array corresponds to a reflection
-
# in the #chain.
-
2
def scope_chain
-
11
scope ? [[scope]] : [[]]
-
end
-
-
2
def has_inverse?
-
7
inverse_name
-
end
-
-
2
def polymorphic_inverse_of(associated_class)
-
if has_inverse?
-
if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
-
inverse_relationship
-
else
-
raise InverseOfAssociationNotFoundError.new(self, associated_class)
-
end
-
end
-
end
-
-
# Returns the macro type.
-
#
-
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
-
2
def macro; raise NotImplementedError; end
-
-
# Returns whether or not this association reflection is for a collection
-
# association. Returns +true+ if the +macro+ is either +has_many+ or
-
# +has_and_belongs_to_many+, +false+ otherwise.
-
2
def collection?
-
28
false
-
end
-
-
# Returns whether or not the association should be validated as part of
-
# the parent's validation.
-
#
-
# Unless you explicitly disable validation with
-
# <tt>validate: false</tt>, validation will take place when:
-
#
-
# * you explicitly enable validation; <tt>validate: true</tt>
-
# * you use autosave; <tt>autosave: true</tt>
-
# * the association is a +has_many+ association
-
2
def validate?
-
32
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
-
end
-
-
# Returns +true+ if +self+ is a +belongs_to+ reflection.
-
18
def belongs_to?; false; end
-
-
# Returns +true+ if +self+ is a +has_one+ reflection.
-
10
def has_one?; false; end
-
-
2
def association_class
-
7
case macro
-
when :belongs_to
-
4
if polymorphic?
-
Associations::BelongsToPolymorphicAssociation
-
else
-
4
Associations::BelongsToAssociation
-
end
-
when :has_many
-
3
if options[:through]
-
Associations::HasManyThroughAssociation
-
else
-
3
Associations::HasManyAssociation
-
end
-
when :has_one
-
if options[:through]
-
Associations::HasOneThroughAssociation
-
else
-
Associations::HasOneAssociation
-
end
-
end
-
end
-
-
2
def polymorphic?
-
51
options[:polymorphic]
-
end
-
-
2
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
-
2
INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
-
-
2
protected
-
-
2
def actual_source_reflection # FIXME: this is a horrible name
-
self
-
end
-
-
2
private
-
-
2
def calculate_constructable(macro, options)
-
24
case macro
-
when :belongs_to
-
8
!polymorphic?
-
when :has_one
-
!options[:through]
-
else
-
16
true
-
end
-
end
-
-
# Attempts to find the inverse association name automatically.
-
# If it cannot find a suitable inverse association name, it returns
-
# nil.
-
2
def inverse_name
-
12
options.fetch(:inverse_of) do
-
12
if @automatic_inverse_of == false
-
4
nil
-
else
-
8
@automatic_inverse_of ||= automatic_inverse_of
-
end
-
end
-
end
-
-
# returns either nil or the inverse association name that it finds.
-
2
def automatic_inverse_of
-
2
if can_find_inverse_of_automatically?(self)
-
2
inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
-
-
2
begin
-
2
reflection = klass._reflect_on_association(inverse_name)
-
rescue NameError
-
# Give up: we couldn't compute the klass type so we won't be able
-
# to find any associations either.
-
reflection = false
-
end
-
-
2
if valid_inverse_reflection?(reflection)
-
1
return inverse_name
-
end
-
end
-
-
1
false
-
end
-
-
# Checks if the inverse reflection that is returned from the
-
# +automatic_inverse_of+ method is a valid reflection. We must
-
# make sure that the reflection's active_record name matches up
-
# with the current reflection's klass name.
-
#
-
# Note: klass will always be valid because when there's a NameError
-
# from calling +klass+, +reflection+ will already be set to false.
-
2
def valid_inverse_reflection?(reflection)
-
reflection &&
-
2
klass.name == reflection.active_record.name &&
-
can_find_inverse_of_automatically?(reflection)
-
end
-
-
# Checks to see if the reflection doesn't have any options that prevent
-
# us from being able to guess the inverse automatically. First, the
-
# <tt>inverse_of</tt> option cannot be set to false. Second, we must
-
# have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
-
# Third, we must not have options such as <tt>:polymorphic</tt> or
-
# <tt>:foreign_key</tt> which prevent us from correctly guessing the
-
# inverse association.
-
#
-
# Anything with a scope can additionally ruin our attempt at finding an
-
# inverse, so we exclude reflections with scopes.
-
2
def can_find_inverse_of_automatically?(reflection)
-
reflection.options[:inverse_of] != false &&
-
3
VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
-
12
!INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
-
!reflection.scope
-
end
-
-
2
def derive_class_name
-
16
class_name = name.to_s
-
16
class_name = class_name.singularize if collection?
-
16
class_name.camelize
-
end
-
-
2
def derive_foreign_key
-
16
if belongs_to?
-
8
"#{name}_id"
-
8
elsif options[:as]
-
"#{options[:as]}_id"
-
else
-
8
active_record.name.foreign_key
-
end
-
end
-
-
2
def derive_join_table
-
ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
-
end
-
-
2
def primary_key(klass)
-
6
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
-
end
-
end
-
-
2
class HasManyReflection < AssociationReflection # :nodoc:
-
2
def initialize(name, scope, options, active_record)
-
16
super(name, scope, options, active_record)
-
end
-
-
66
def macro; :has_many; end
-
-
64
def collection?; true; end
-
end
-
-
2
class HasOneReflection < AssociationReflection # :nodoc:
-
2
def initialize(name, scope, options, active_record)
-
super(name, scope, options, active_record)
-
end
-
-
2
def macro; :has_one; end
-
-
2
def has_one?; true; end
-
end
-
-
2
class BelongsToReflection < AssociationReflection # :nodoc:
-
2
def initialize(name, scope, options, active_record)
-
8
super(name, scope, options, active_record)
-
end
-
-
40
def macro; :belongs_to; end
-
-
20
def belongs_to?; true; end
-
-
2
def join_keys(assoc_klass)
-
5
key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key
-
5
JoinKeys.new(key, foreign_key)
-
end
-
-
2
def join_id_for(owner) # :nodoc:
-
3
owner[foreign_key]
-
end
-
end
-
-
2
class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
-
2
def initialize(name, scope, options, active_record)
-
super
-
end
-
-
2
def macro; :has_and_belongs_to_many; end
-
-
2
def collection?
-
true
-
end
-
end
-
-
# Holds all the meta-data about a :through association as it was specified
-
# in the Active Record class.
-
2
class ThroughReflection < AbstractReflection #:nodoc:
-
2
attr_reader :delegate_reflection
-
2
delegate :foreign_key, :foreign_type, :association_foreign_key,
-
:active_record_primary_key, :type, :to => :source_reflection
-
-
2
def initialize(delegate_reflection)
-
8
@delegate_reflection = delegate_reflection
-
8
@klass = delegate_reflection.options[:anonymous_class]
-
8
@source_reflection_name = delegate_reflection.options[:source]
-
end
-
-
2
def klass
-
36
@klass ||= delegate_reflection.compute_class(class_name)
-
end
-
-
# Returns the source of the through reflection. It checks both a singularized
-
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :taggings
-
# has_many :tags, through: :taggings
-
# end
-
#
-
# class Tagging < ActiveRecord::Base
-
# belongs_to :post
-
# belongs_to :tag
-
# end
-
#
-
# tags_reflection = Post.reflect_on_association(:tags)
-
# tags_reflection.source_reflection
-
# # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags">
-
#
-
2
def source_reflection
-
26
through_reflection.klass._reflect_on_association(source_reflection_name)
-
end
-
-
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
-
# of a HasManyThrough or HasOneThrough association.
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :taggings
-
# has_many :tags, through: :taggings
-
# end
-
#
-
# tags_reflection = Post.reflect_on_association(:tags)
-
# tags_reflection.through_reflection
-
# # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings">
-
#
-
2
def through_reflection
-
78
active_record._reflect_on_association(options[:through])
-
end
-
-
# Returns an array of reflections which are involved in this association. Each item in the
-
# array corresponds to a table which will be part of the query for this association.
-
#
-
# The chain is built by recursively calling #chain on the source reflection and the through
-
# reflection. The base case for the recursion is a normal association, which just returns
-
# [self] as its #chain.
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :taggings
-
# has_many :tags, through: :taggings
-
# end
-
#
-
# tags_reflection = Post.reflect_on_association(:tags)
-
# tags_reflection.chain
-
# # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
-
# <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>]
-
#
-
2
def chain
-
@chain ||= begin
-
a = source_reflection.chain
-
b = through_reflection.chain
-
chain = a + b
-
chain[0] = self # Use self so we don't lose the information from :source_type
-
chain
-
end
-
end
-
-
# Consider the following example:
-
#
-
# class Person
-
# has_many :articles
-
# has_many :comment_tags, through: :articles
-
# end
-
#
-
# class Article
-
# has_many :comments
-
# has_many :comment_tags, through: :comments, source: :tags
-
# end
-
#
-
# class Comment
-
# has_many :tags
-
# end
-
#
-
# There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags,
-
# but only Comment.tags will be represented in the #chain. So this method creates an array
-
# of scopes corresponding to the chain.
-
2
def scope_chain
-
@scope_chain ||= begin
-
scope_chain = source_reflection.scope_chain.map(&:dup)
-
-
# Add to it the scope from this reflection (if any)
-
scope_chain.first << scope if scope
-
-
through_scope_chain = through_reflection.scope_chain.map(&:dup)
-
-
if options[:source_type]
-
type = foreign_type
-
source_type = options[:source_type]
-
through_scope_chain.first << lambda { |object|
-
where(type => source_type)
-
}
-
end
-
-
# Recursively fill out the rest of the array from the through reflection
-
scope_chain + through_scope_chain
-
end
-
end
-
-
2
def join_keys(assoc_klass)
-
source_reflection.join_keys(assoc_klass)
-
end
-
-
# The macro used by the source association
-
2
def source_macro
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
ActiveRecord::Base.source_macro is deprecated and will be removed
-
without replacement.
-
MSG
-
-
source_reflection.source_macro
-
end
-
-
# A through association is nested if there would be more than one join table
-
2
def nested?
-
chain.length > 2
-
end
-
-
# We want to use the klass from this reflection, rather than just delegate straight to
-
# the source_reflection, because the source_reflection may be polymorphic. We still
-
# need to respect the source_reflection's :primary_key option, though.
-
2
def association_primary_key(klass = nil)
-
# Get the "actual" source reflection if the immediate source reflection has a
-
# source reflection itself
-
actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
-
end
-
-
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
-
#
-
# class Post < ActiveRecord::Base
-
# has_many :taggings
-
# has_many :tags, through: :taggings
-
# end
-
#
-
# tags_reflection = Post.reflect_on_association(:tags)
-
# tags_reflection.source_reflection_names
-
# # => [:tag, :tags]
-
#
-
2
def source_reflection_names
-
options[:source] ? [options[:source]] : [name.to_s.singularize, name].uniq
-
end
-
-
2
def source_reflection_name # :nodoc:
-
26
return @source_reflection_name if @source_reflection_name
-
-
24
names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
-
8
names = names.find_all { |n|
-
16
through_reflection.klass._reflect_on_association(n)
-
}
-
-
8
if names.length > 1
-
example_options = options.dup
-
example_options[:source] = source_reflection_names.first
-
ActiveSupport::Deprecation.warn \
-
"Ambiguous source reflection for through association. Please " \
-
"specify a :source directive on your declaration like:\n" \
-
"\n" \
-
" class #{active_record.name} < ActiveRecord::Base\n" \
-
" #{macro} :#{name}, #{example_options}\n" \
-
" end"
-
end
-
-
8
@source_reflection_name = names.first
-
end
-
-
2
def source_options
-
source_reflection.options
-
end
-
-
2
def through_options
-
through_reflection.options
-
end
-
-
2
def join_id_for(owner) # :nodoc:
-
source_reflection.join_id_for(owner)
-
end
-
-
2
def check_validity!
-
if through_reflection.nil?
-
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
-
end
-
-
if through_reflection.polymorphic?
-
if has_one?
-
raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self)
-
else
-
raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
-
end
-
end
-
-
if source_reflection.nil?
-
raise HasManyThroughSourceAssociationNotFoundError.new(self)
-
end
-
-
if options[:source_type] && !source_reflection.polymorphic?
-
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
-
end
-
-
if source_reflection.polymorphic? && options[:source_type].nil?
-
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
-
end
-
-
if has_one? && through_reflection.collection?
-
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
-
end
-
-
check_validity_of_inverse!
-
end
-
-
2
protected
-
-
2
def actual_source_reflection # FIXME: this is a horrible name
-
source_reflection.send(:actual_source_reflection)
-
end
-
-
2
def primary_key(klass)
-
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
-
end
-
-
2
def inverse_name; delegate_reflection.send(:inverse_name); end
-
-
2
private
-
2
def derive_class_name
-
# get the class_name of the belongs_to association of the through reflection
-
8
options[:source_type] || source_reflection.class_name
-
end
-
-
2
delegate_methods = AssociationReflection.public_instance_methods -
-
public_instance_methods
-
-
2
delegate(*delegate_methods, to: :delegate_reflection)
-
-
end
-
end
-
end
-
# -*- coding: utf-8 -*-
-
2
require 'arel/collectors/bind'
-
-
2
module ActiveRecord
-
# = Active Record Relation
-
2
class Relation
-
2
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
-
:order, :joins, :where, :having, :bind, :references,
-
:extending, :unscope]
-
-
2
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
-
:reverse_order, :distinct, :create_with, :uniq]
-
2
INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
-
-
2
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
-
-
2
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
-
-
2
attr_reader :table, :klass, :loaded
-
2
alias :model :klass
-
2
alias :loaded? :loaded
-
-
2
def initialize(klass, table, values = {})
-
292
@klass = klass
-
292
@table = table
-
292
@values = values
-
292
@offsets = {}
-
292
@loaded = false
-
end
-
-
2
def initialize_copy(other)
-
# This method is a hot spot, so for now, use Hash[] to dup the hash.
-
# https://bugs.ruby-lang.org/issues/7166
-
391
@values = Hash[@values]
-
391
@values[:bind] = @values[:bind].dup if @values.key? :bind
-
391
reset
-
end
-
-
2
def insert(values) # :nodoc:
-
6
primary_key_value = nil
-
-
6
if primary_key && Hash === values
-
6
primary_key_value = values[values.keys.find { |k|
-
22
k.name == primary_key
-
}]
-
-
6
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
-
primary_key_value = connection.next_sequence_value(klass.sequence_name)
-
values[klass.arel_table[klass.primary_key]] = primary_key_value
-
end
-
end
-
-
6
im = arel.create_insert
-
6
im.into @table
-
-
6
substitutes, binds = substitute_values values
-
-
6
if values.empty? # empty insert
-
im.values = Arel.sql(connection.empty_insert_statement_value)
-
else
-
6
im.insert substitutes
-
end
-
-
6
@klass.connection.insert(
-
im,
-
'SQL',
-
primary_key,
-
primary_key_value,
-
nil,
-
binds)
-
end
-
-
2
def _update_record(values, id, id_was) # :nodoc:
-
2
substitutes, binds = substitute_values values
-
-
2
scope = @klass.unscoped
-
-
2
if @klass.finder_needs_type_condition?
-
scope.unscope!(where: @klass.inheritance_column)
-
end
-
-
2
relation = scope.where(@klass.primary_key => (id_was || id))
-
2
bvs = binds + relation.bind_values
-
2
um = relation
-
.arel
-
.compile_update(substitutes, @klass.primary_key)
-
-
2
@klass.connection.update(
-
um,
-
'SQL',
-
bvs,
-
)
-
end
-
-
2
def substitute_values(values) # :nodoc:
-
8
binds = values.map do |arel_attr, value|
-
27
[@klass.columns_hash[arel_attr.name], value]
-
end
-
-
8
substitutes = values.each_with_index.map do |(arel_attr, _), i|
-
27
[arel_attr, @klass.connection.substitute_at(binds[i][0])]
-
end
-
-
8
[substitutes, binds]
-
end
-
-
# Initializes new record from relation while maintaining the current
-
# scope.
-
#
-
# Expects arguments in the same format as +Base.new+.
-
#
-
# users = User.where(name: 'DHH')
-
# user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
-
#
-
# You can also pass a block to new with the new record as argument:
-
#
-
# user = users.new { |user| user.name = 'Oscar' }
-
# user.name # => Oscar
-
2
def new(*args, &block)
-
scoping { @klass.new(*args, &block) }
-
end
-
-
2
alias build new
-
-
# Tries to create a new record with the same scoped attributes
-
# defined in the relation. Returns the initialized object if validation fails.
-
#
-
# Expects arguments in the same format as +Base.create+.
-
#
-
# ==== Examples
-
# users = User.where(name: 'Oscar')
-
# users.create # #<User id: 3, name: "oscar", ...>
-
#
-
# users.create(name: 'fxn')
-
# users.create # #<User id: 4, name: "fxn", ...>
-
#
-
# users.create { |user| user.name = 'tenderlove' }
-
# # #<User id: 5, name: "tenderlove", ...>
-
#
-
# users.create(name: nil) # validation on name
-
# # #<User id: nil, name: nil, ...>
-
2
def create(*args, &block)
-
scoping { @klass.create(*args, &block) }
-
end
-
-
# Similar to #create, but calls +create!+ on the base class. Raises
-
# an exception if a validation error occurs.
-
#
-
# Expects arguments in the same format as <tt>Base.create!</tt>.
-
2
def create!(*args, &block)
-
scoping { @klass.create!(*args, &block) }
-
end
-
-
2
def first_or_create(attributes = nil, &block) # :nodoc:
-
first || create(attributes, &block)
-
end
-
-
2
def first_or_create!(attributes = nil, &block) # :nodoc:
-
first || create!(attributes, &block)
-
end
-
-
2
def first_or_initialize(attributes = nil, &block) # :nodoc:
-
first || new(attributes, &block)
-
end
-
-
# Finds the first record with the given attributes, or creates a record
-
# with the attributes if one is not found:
-
#
-
# # Find the first user named "Penélope" or create a new one.
-
# User.find_or_create_by(first_name: 'Penélope')
-
# # => #<User id: 1, first_name: "Penélope", last_name: nil>
-
#
-
# # Find the first user named "Penélope" or create a new one.
-
# # We already have one so the existing record will be returned.
-
# User.find_or_create_by(first_name: 'Penélope')
-
# # => #<User id: 1, first_name: "Penélope", last_name: nil>
-
#
-
# # Find the first user named "Scarlett" or create a new one with
-
# # a particular last name.
-
# User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
-
# # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
-
#
-
# This method accepts a block, which is passed down to +create+. The last example
-
# above can be alternatively written this way:
-
#
-
# # Find the first user named "Scarlett" or create a new one with a
-
# # different last name.
-
# User.find_or_create_by(first_name: 'Scarlett') do |user|
-
# user.last_name = 'Johansson'
-
# end
-
# # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
-
#
-
# This method always returns a record, but if creation was attempted and
-
# failed due to validation errors it won't be persisted, you get what
-
# +create+ returns in such situation.
-
#
-
# Please note *this method is not atomic*, it runs first a SELECT, and if
-
# there are no results an INSERT is attempted. If there are other threads
-
# or processes there is a race condition between both calls and it could
-
# be the case that you end up with two similar records.
-
#
-
# Whether that is a problem or not depends on the logic of the
-
# application, but in the particular case in which rows have a UNIQUE
-
# constraint an exception may be raised, just retry:
-
#
-
# begin
-
# CreditAccount.find_or_create_by(user_id: user.id)
-
# rescue ActiveRecord::RecordNotUnique
-
# retry
-
# end
-
#
-
2
def find_or_create_by(attributes, &block)
-
find_by(attributes) || create(attributes, &block)
-
end
-
-
# Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
-
# is raised if the created record is invalid.
-
2
def find_or_create_by!(attributes, &block)
-
find_by(attributes) || create!(attributes, &block)
-
end
-
-
# Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
-
2
def find_or_initialize_by(attributes, &block)
-
find_by(attributes) || new(attributes, &block)
-
end
-
-
# Runs EXPLAIN on the query or queries triggered by this relation and
-
# returns the result as a string. The string is formatted imitating the
-
# ones printed by the database shell.
-
#
-
# Note that this method actually runs the queries, since the results of some
-
# are needed by the next ones when eager loading is going on.
-
#
-
# Please see further details in the
-
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
-
2
def explain
-
#TODO: Fix for binds.
-
exec_explain(collecting_queries_for_explain { exec_queries })
-
end
-
-
# Converts relation objects to Array.
-
2
def to_a
-
96
load
-
96
@records
-
end
-
-
# Serializes the relation objects Array.
-
2
def encode_with(coder)
-
coder.represent_seq(nil, to_a)
-
end
-
-
2
def as_json(options = nil) #:nodoc:
-
to_a.as_json(options)
-
end
-
-
# Returns size of the records.
-
2
def size
-
loaded? ? @records.length : count(:all)
-
end
-
-
# Returns true if there are no records.
-
2
def empty?
-
return @records.empty? if loaded?
-
-
if limit_value == 0
-
true
-
else
-
c = count(:all)
-
c.respond_to?(:zero?) ? c.zero? : c.empty?
-
end
-
end
-
-
# Returns true if there are any records.
-
2
def any?
-
if block_given?
-
to_a.any? { |*block_args| yield(*block_args) }
-
else
-
!empty?
-
end
-
end
-
-
# Returns true if there is more than one record.
-
2
def many?
-
if block_given?
-
to_a.many? { |*block_args| yield(*block_args) }
-
else
-
limit_value ? to_a.many? : size > 1
-
end
-
end
-
-
# Scope all queries to the current scope.
-
#
-
# Comment.where(post_id: 1).scoping do
-
# Comment.first
-
# end
-
# # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
-
#
-
# Please check unscoped if you want to remove all previous scopes (including
-
# the default_scope) during the execution of a block.
-
2
def scoping
-
111
previous, klass.current_scope = klass.current_scope, self
-
111
yield
-
ensure
-
111
klass.current_scope = previous
-
end
-
-
# Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
-
# statement and sends it straight to the database. It does not instantiate the involved models and it does not
-
# trigger Active Record callbacks or validations. Values passed to `update_all` will not go through
-
# ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL
-
# database.
-
#
-
# ==== Parameters
-
#
-
# * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
-
#
-
# ==== Examples
-
#
-
# # Update all customers with the given attributes
-
# Customer.update_all wants_email: true
-
#
-
# # Update all books with 'Rails' in their title
-
# Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
-
#
-
# # Update all books that match conditions, but limit it to 5 ordered by date
-
# Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David')
-
2
def update_all(updates)
-
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
-
-
stmt = Arel::UpdateManager.new(arel.engine)
-
-
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
-
stmt.table(table)
-
stmt.key = table[primary_key]
-
-
if joins_values.any?
-
@klass.connection.join_to_update(stmt, arel)
-
else
-
stmt.take(arel.limit)
-
stmt.order(*arel.orders)
-
stmt.wheres = arel.constraints
-
end
-
-
bvs = arel.bind_values + bind_values
-
@klass.connection.update stmt, 'SQL', bvs
-
end
-
-
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
-
# The resulting object is returned whether the object was saved successfully to the database or not.
-
#
-
# ==== Parameters
-
#
-
# * +id+ - This should be the id or an array of ids to be updated.
-
# * +attributes+ - This should be a hash of attributes or an array of hashes.
-
#
-
# ==== Examples
-
#
-
# # Updates one record
-
# Person.update(15, user_name: 'Samuel', group: 'expert')
-
#
-
# # Updates multiple records
-
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
-
# Person.update(people.keys, people.values)
-
2
def update(id, attributes)
-
if id.is_a?(Array)
-
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
-
else
-
object = find(id)
-
object.update(attributes)
-
object
-
end
-
end
-
-
# Destroys the records matching +conditions+ by instantiating each
-
# record and calling its +destroy+ method. Each object's callbacks are
-
# executed (including <tt>:dependent</tt> association options). Returns the
-
# collection of objects that were destroyed; each will be frozen, to
-
# reflect that no changes should be made (since they can't be persisted).
-
#
-
# Note: Instantiation, callback execution, and deletion of each
-
# record can be time consuming when you're removing many records at
-
# once. It generates at least one SQL +DELETE+ query per record (or
-
# possibly more, to enforce your callbacks). If you want to delete many
-
# rows quickly, without concern for their associations or callbacks, use
-
# +delete_all+ instead.
-
#
-
# ==== Parameters
-
#
-
# * +conditions+ - A string, array, or hash that specifies which records
-
# to destroy. If omitted, all records are destroyed. See the
-
# Conditions section in the introduction to ActiveRecord::Base for
-
# more information.
-
#
-
# ==== Examples
-
#
-
# Person.destroy_all("last_login < '2004-04-04'")
-
# Person.destroy_all(status: "inactive")
-
# Person.where(age: 0..18).destroy_all
-
2
def destroy_all(conditions = nil)
-
if conditions
-
where(conditions).destroy_all
-
else
-
to_a.each {|object| object.destroy }.tap { reset }
-
end
-
end
-
-
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
-
# therefore all callbacks and filters are fired off before the object is deleted. This method is
-
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
-
#
-
# This essentially finds the object (or multiple objects) with the given id, creates a new object
-
# from the attributes, and then calls destroy on it.
-
#
-
# ==== Parameters
-
#
-
# * +id+ - Can be either an Integer or an Array of Integers.
-
#
-
# ==== Examples
-
#
-
# # Destroy a single object
-
# Todo.destroy(1)
-
#
-
# # Destroy multiple objects
-
# todos = [1,2,3]
-
# Todo.destroy(todos)
-
2
def destroy(id)
-
if id.is_a?(Array)
-
id.map { |one_id| destroy(one_id) }
-
else
-
find(id).destroy
-
end
-
end
-
-
# Deletes the records matching +conditions+ without instantiating the records
-
# first, and hence not calling the +destroy+ method nor invoking callbacks. This
-
# is a single SQL DELETE statement that goes straight to the database, much more
-
# efficient than +destroy_all+. Be careful with relations though, in particular
-
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
-
# number of rows affected.
-
#
-
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
-
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
-
# Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
-
#
-
# Both calls delete the affected posts all at once with a single DELETE statement.
-
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
-
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
-
#
-
# If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
-
#
-
# Post.limit(100).delete_all
-
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
-
2
def delete_all(conditions = nil)
-
3
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
-
15
if MULTI_VALUE_METHODS.include?(method)
-
6
send("#{method}_values").any?
-
else
-
9
send("#{method}_value")
-
end
-
}
-
3
if invalid_methods.any?
-
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
-
end
-
-
3
if conditions
-
where(conditions).delete_all
-
else
-
3
stmt = Arel::DeleteManager.new(arel.engine)
-
3
stmt.from(table)
-
-
3
if joins_values.any?
-
@klass.connection.join_to_delete(stmt, arel, table[primary_key])
-
else
-
3
stmt.wheres = arel.constraints
-
end
-
-
3
bvs = arel.bind_values + bind_values
-
3
affected = @klass.connection.delete(stmt, 'SQL', bvs)
-
-
3
reset
-
3
affected
-
end
-
end
-
-
# Deletes the row with a primary key matching the +id+ argument, using a
-
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
-
# Record objects are not instantiated, so the object's callbacks are not
-
# executed, including any <tt>:dependent</tt> association options.
-
#
-
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
-
#
-
# Note: Although it is often much faster than the alternative,
-
# <tt>#destroy</tt>, skipping callbacks might bypass business logic in
-
# your application that ensures referential integrity or performs other
-
# essential jobs.
-
#
-
# ==== Examples
-
#
-
# # Delete a single row
-
# Todo.delete(1)
-
#
-
# # Delete multiple rows
-
# Todo.delete([2,3,4])
-
2
def delete(id_or_array)
-
where(primary_key => id_or_array).delete_all
-
end
-
-
# Causes the records to be loaded from the database if they have not
-
# been loaded already. You can use this if for some reason you need
-
# to explicitly load some records before actually using them. The
-
# return value is the relation itself, not the records.
-
#
-
# Post.where(published: true).load # => #<ActiveRecord::Relation>
-
2
def load
-
96
exec_queries unless loaded?
-
-
96
self
-
end
-
-
# Forces reloading of relation.
-
2
def reload
-
reset
-
load
-
end
-
-
2
def reset
-
394
@last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
-
394
@should_eager_load = @join_dependency = nil
-
394
@records = []
-
394
@offsets = {}
-
394
self
-
end
-
-
# Returns sql statement for the relation.
-
#
-
# User.where(name: 'Oscar').to_sql
-
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
-
2
def to_sql
-
@to_sql ||= begin
-
relation = self
-
connection = klass.connection
-
visitor = connection.visitor
-
-
if eager_loading?
-
find_with_associations { |rel| relation = rel }
-
end
-
-
arel = relation.arel
-
binds = (arel.bind_values + relation.bind_values).dup
-
binds.map! { |bv| connection.quote(*bv.reverse) }
-
collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new)
-
collect.substitute_binds(binds).join
-
end
-
end
-
-
# Returns a hash of where conditions.
-
#
-
# User.where(name: 'Oscar').where_values_hash
-
# # => {name: "Oscar"}
-
2
def where_values_hash(relation_table_name = table_name)
-
equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
-
node.left.relation.name == relation_table_name
-
}
-
-
binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
-
-
Hash[equalities.map { |where|
-
name = where.left.name
-
[name, binds.fetch(name.to_s) {
-
case where.right
-
when Array then where.right.map(&:val)
-
when Arel::Nodes::Casted
-
where.right.val
-
end
-
}]
-
}]
-
end
-
-
2
def scope_for_create
-
@scope_for_create ||= where_values_hash.merge(create_with_value)
-
end
-
-
# Returns true if relation needs eager loading.
-
2
def eager_loading?
-
@should_eager_load ||=
-
eager_load_values.any? ||
-
219
includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
-
end
-
-
# Joins that are also marked for preloading. In which case we should just eager load them.
-
# Note that this is a naive implementation because we could have strings and symbols which
-
# represent the same association, but that aren't matched by this. Also, we could have
-
# nested hashes which partially match, e.g. { a: :b } & { a: [:b, :c] }
-
2
def joined_includes_values
-
includes_values & joins_values
-
end
-
-
# +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
-
# to maintain backwards compatibility. Use +distinct_value+ instead.
-
2
def uniq_value
-
distinct_value
-
end
-
-
# Compares two relations for equality.
-
2
def ==(other)
-
case other
-
when Associations::CollectionProxy, AssociationRelation
-
self == other.to_a
-
when Relation
-
other.to_sql == to_sql
-
when Array
-
to_a == other
-
end
-
end
-
-
2
def pretty_print(q)
-
q.pp(self.to_a)
-
end
-
-
# Returns true if relation is blank.
-
2
def blank?
-
to_a.blank?
-
end
-
-
2
def values
-
55
Hash[@values]
-
end
-
-
2
def inspect
-
entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect)
-
entries[10] = '...' if entries.size == 11
-
-
"#<#{self.class.name} [#{entries.join(', ')}]>"
-
end
-
-
2
private
-
-
2
def exec_queries
-
96
@records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
-
-
96
preload = preload_values
-
96
preload += includes_values unless eager_loading?
-
96
preloader = build_preloader
-
96
preload.each do |associations|
-
preloader.preload @records, associations
-
end
-
-
96
@records.each { |record| record.readonly! } if readonly_value
-
-
96
@loaded = true
-
96
@records
-
end
-
-
2
def build_preloader
-
96
ActiveRecord::Associations::Preloader.new
-
end
-
-
2
def references_eager_loaded_tables?
-
joined_tables = arel.join_sources.map do |join|
-
if join.is_a?(Arel::Nodes::StringJoin)
-
tables_in_string(join.left)
-
else
-
[join.left.table_name, join.left.table_alias]
-
end
-
end
-
-
joined_tables += [table.name, table.table_alias]
-
-
# always convert table names to downcase as in Oracle quoted table names are in uppercase
-
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
-
-
(references_values - joined_tables).any?
-
end
-
-
2
def tables_in_string(string)
-
return [] if string.blank?
-
# always convert table names to downcase as in Oracle quoted table names are in uppercase
-
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
-
string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Batches
-
# Looping through a collection of records from the database
-
# (using the +all+ method, for example) is very inefficient
-
# since it will try to instantiate all the objects at once.
-
#
-
# In that case, batch processing methods allow you to work
-
# with the records in batches, thereby greatly reducing memory consumption.
-
#
-
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
-
# specified by the +:batch_size+ option).
-
#
-
# Person.find_each do |person|
-
# person.do_awesome_stuff
-
# end
-
#
-
# Person.where("age > 21").find_each do |person|
-
# person.party_all_night!
-
# end
-
#
-
# If you do not provide a block to #find_each, it will return an Enumerator
-
# for chaining with other methods:
-
#
-
# Person.find_each.with_index do |person, index|
-
# person.award_trophy(index + 1)
-
# end
-
#
-
# ==== Options
-
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
-
# * <tt>:start</tt> - Specifies the starting point for the batch processing.
-
# This is especially useful if you want multiple workers dealing with
-
# the same processing queue. You can make worker 1 handle all the records
-
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
-
# (by setting the +:start+ option on that worker).
-
#
-
# # Let's process for a batch of 2000 records, skipping the first 2000 rows
-
# Person.find_each(start: 2000, batch_size: 2000) do |person|
-
# person.party_all_night!
-
# end
-
#
-
# NOTE: It's not possible to set the order. That is automatically set to
-
# ascending on the primary key ("id ASC") to make the batch ordering
-
# work. This also means that this method only works with integer-based
-
# primary keys.
-
#
-
# NOTE: You can't set the limit either, that's used to control
-
# the batch sizes.
-
2
def find_each(options = {})
-
if block_given?
-
find_in_batches(options) do |records|
-
records.each { |record| yield record }
-
end
-
else
-
enum_for :find_each, options do
-
options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
-
end
-
end
-
end
-
-
# Yields each batch of records that was found by the find +options+ as
-
# an array.
-
#
-
# Person.where("age > 21").find_in_batches do |group|
-
# sleep(50) # Make sure it doesn't get too crowded in there!
-
# group.each { |person| person.party_all_night! }
-
# end
-
#
-
# If you do not provide a block to #find_in_batches, it will return an Enumerator
-
# for chaining with other methods:
-
#
-
# Person.find_in_batches.with_index do |group, batch|
-
# puts "Processing group ##{batch}"
-
# group.each(&:recover_from_last_night!)
-
# end
-
#
-
# To be yielded each record one by one, use #find_each instead.
-
#
-
# ==== Options
-
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
-
# * <tt>:start</tt> - Specifies the starting point for the batch processing.
-
# This is especially useful if you want multiple workers dealing with
-
# the same processing queue. You can make worker 1 handle all the records
-
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
-
# (by setting the +:start+ option on that worker).
-
#
-
# # Let's process the next 2000 records
-
# Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
-
# group.each { |person| person.party_all_night! }
-
# end
-
#
-
# NOTE: It's not possible to set the order. That is automatically set to
-
# ascending on the primary key ("id ASC") to make the batch ordering
-
# work. This also means that this method only works with integer-based
-
# primary keys.
-
#
-
# NOTE: You can't set the limit either, that's used to control
-
# the batch sizes.
-
2
def find_in_batches(options = {})
-
options.assert_valid_keys(:start, :batch_size)
-
-
relation = self
-
start = options[:start]
-
batch_size = options[:batch_size] || 1000
-
-
unless block_given?
-
return to_enum(:find_in_batches, options) do
-
total = start ? where(table[primary_key].gteq(start)).size : size
-
(total - 1).div(batch_size) + 1
-
end
-
end
-
-
if logger && (arel.orders.present? || arel.taken.present?)
-
logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
-
end
-
-
relation = relation.reorder(batch_order).limit(batch_size)
-
records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
-
-
while records.any?
-
records_size = records.size
-
primary_key_offset = records.last.id
-
raise "Primary key not included in the custom select clause" unless primary_key_offset
-
-
yield records
-
-
break if records_size < batch_size
-
-
records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
-
end
-
end
-
-
2
private
-
-
2
def batch_order
-
"#{quoted_table_name}.#{quoted_primary_key} ASC"
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Calculations
-
# Count the records.
-
#
-
# Person.count
-
# # => the total count of all people
-
#
-
# Person.count(:age)
-
# # => returns the total count of all people whose age is present in database
-
#
-
# Person.count(:all)
-
# # => performs a COUNT(*) (:all is an alias for '*')
-
#
-
# Person.distinct.count(:age)
-
# # => counts the number of different age values
-
#
-
# If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column,
-
# and the values are the respective amounts:
-
#
-
# Person.group(:city).count
-
# # => { 'Rome' => 5, 'Paris' => 3 }
-
#
-
# If +count+ is used with +group+ for multiple columns, it returns a Hash whose
-
# keys are an array containing the individual values of each column and the value
-
# of each key would be the +count+.
-
#
-
# Article.group(:status, :category).count
-
# # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
-
# ["published", "business"]=>0, ["published", "technology"]=>2}
-
#
-
# If +count+ is used with +select+, it will count the selected columns:
-
#
-
# Person.select(:age).count
-
# # => counts the number of different age values
-
#
-
# Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
-
# between databases. In invalid cases, an error from the database is thrown.
-
2
def count(column_name = nil, options = {})
-
# TODO: Remove options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
19
column_name, options = nil, column_name if column_name.is_a?(Hash)
-
19
calculate(:count, column_name, options)
-
end
-
-
# Calculates the average value on a given column. Returns +nil+ if there's
-
# no row. See +calculate+ for examples with options.
-
#
-
# Person.average(:age) # => 35.8
-
2
def average(column_name, options = {})
-
# TODO: Remove options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
calculate(:average, column_name, options)
-
end
-
-
# Calculates the minimum value on a given column. The value is returned
-
# with the same data type of the column, or +nil+ if there's no row. See
-
# +calculate+ for examples with options.
-
#
-
# Person.minimum(:age) # => 7
-
2
def minimum(column_name, options = {})
-
# TODO: Remove options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
calculate(:minimum, column_name, options)
-
end
-
-
# Calculates the maximum value on a given column. The value is returned
-
# with the same data type of the column, or +nil+ if there's no row. See
-
# +calculate+ for examples with options.
-
#
-
# Person.maximum(:age) # => 93
-
2
def maximum(column_name, options = {})
-
# TODO: Remove options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
calculate(:maximum, column_name, options)
-
end
-
-
# Calculates the sum of values on a given column. The value is returned
-
# with the same data type of the column, 0 if there's no row. See
-
# +calculate+ for examples with options.
-
#
-
# Person.sum(:age) # => 4562
-
2
def sum(*args)
-
calculate(:sum, *args)
-
end
-
-
# This calculates aggregate values in the given column. Methods for count, sum, average,
-
# minimum, and maximum have been added as shortcuts.
-
#
-
# There are two basic forms of output:
-
#
-
# * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
-
# for AVG, and the given column's type for everything else.
-
#
-
# * Grouped values: This returns an ordered hash of the values and groups them. It
-
# takes either a column name, or the name of a belongs_to association.
-
#
-
# values = Person.group('last_name').maximum(:age)
-
# puts values["Drake"]
-
# # => 43
-
#
-
# drake = Family.find_by(last_name: 'Drake')
-
# values = Person.group(:family).maximum(:age) # Person belongs_to :family
-
# puts values[drake]
-
# # => 43
-
#
-
# values.each do |family, max_age|
-
# ...
-
# end
-
#
-
# Person.calculate(:count, :all) # The same as Person.count
-
# Person.average(:age) # SELECT AVG(age) FROM people...
-
#
-
# # Selects the minimum age for any family without any minors
-
# Person.group(:last_name).having("min(age) > 17").minimum(:age)
-
#
-
# Person.sum("2 * age")
-
2
def calculate(operation, column_name, options = {})
-
# TODO: Remove options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
19
if column_name.is_a?(Symbol) && attribute_alias?(column_name)
-
column_name = attribute_alias(column_name)
-
end
-
-
19
if has_include?(column_name)
-
construct_relation_for_association_calculations.calculate(operation, column_name, options)
-
else
-
19
perform_calculation(operation, column_name, options)
-
end
-
end
-
-
# Use <tt>pluck</tt> as a shortcut to select one or more attributes without
-
# loading a bunch of records just to grab the attributes you want.
-
#
-
# Person.pluck(:name)
-
#
-
# instead of
-
#
-
# Person.all.map(&:name)
-
#
-
# Pluck returns an <tt>Array</tt> of attribute values type-casted to match
-
# the plucked column names, if they can be deduced. Plucking an SQL fragment
-
# returns String values by default.
-
#
-
# Person.pluck(:id)
-
# # SELECT people.id FROM people
-
# # => [1, 2, 3]
-
#
-
# Person.pluck(:id, :name)
-
# # SELECT people.id, people.name FROM people
-
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
-
#
-
# Person.pluck('DISTINCT role')
-
# # SELECT DISTINCT role FROM people
-
# # => ['admin', 'member', 'guest']
-
#
-
# Person.where(age: 21).limit(5).pluck(:id)
-
# # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
-
# # => [2, 3]
-
#
-
# Person.pluck('DATEDIFF(updated_at, created_at)')
-
# # SELECT DATEDIFF(updated_at, created_at) FROM people
-
# # => ['0', '27761', '173']
-
#
-
2
def pluck(*column_names)
-
5
column_names.map! do |column_name|
-
5
if column_name.is_a?(Symbol) && attribute_alias?(column_name)
-
attribute_alias(column_name)
-
else
-
5
column_name.to_s
-
end
-
end
-
-
5
if has_include?(column_names.first)
-
construct_relation_for_association_calculations.pluck(*column_names)
-
else
-
5
relation = spawn
-
5
relation.select_values = column_names.map { |cn|
-
5
columns_hash.key?(cn) ? arel_table[cn] : cn
-
}
-
5
result = klass.connection.select_all(relation.arel, nil, relation.arel.bind_values + bind_values)
-
5
result.cast_values(klass.column_types)
-
end
-
end
-
-
# Pluck all the ID's for the relation using the table's primary key
-
#
-
# Person.ids # SELECT people.id FROM people
-
# Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
-
2
def ids
-
3
pluck primary_key
-
end
-
-
2
private
-
-
2
def has_include?(column_name)
-
24
eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?))
-
end
-
-
2
def perform_calculation(operation, column_name, options = {})
-
# TODO: Remove options argument as soon we remove support to
-
# activerecord-deprecated_finders.
-
19
operation = operation.to_s.downcase
-
-
# If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
-
19
distinct = self.distinct_value
-
-
19
if operation == "count"
-
19
column_name ||= select_for_count
-
-
19
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
-
distinct = true
-
end
-
-
19
column_name = primary_key if column_name == :all && distinct
-
19
distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
-
end
-
-
19
if group_values.any?
-
execute_grouped_calculation(operation, column_name, distinct)
-
else
-
19
execute_simple_calculation(operation, column_name, distinct)
-
end
-
end
-
-
2
def aggregate_column(column_name)
-
19
if @klass.column_names.include?(column_name.to_s)
-
Arel::Attribute.new(@klass.unscoped.table, column_name)
-
else
-
19
Arel.sql(column_name == :all ? "*" : column_name.to_s)
-
end
-
end
-
-
2
def operation_over_aggregate_column(column, operation, distinct)
-
19
operation == 'count' ? column.count(distinct) : column.send(operation)
-
end
-
-
2
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
-
# Postgresql doesn't like ORDER BY when there are no GROUP BY
-
19
relation = unscope(:order)
-
-
19
column_alias = column_name
-
-
19
bind_values = nil
-
-
19
if operation == "count" && (relation.limit_value || relation.offset_value)
-
# Shortcut when limit is zero.
-
return 0 if relation.limit_value == 0
-
-
query_builder = build_count_subquery(relation, column_name, distinct)
-
bind_values = query_builder.bind_values + relation.bind_values
-
else
-
19
column = aggregate_column(column_name)
-
-
19
select_value = operation_over_aggregate_column(column, operation, distinct)
-
-
19
column_alias = select_value.alias
-
19
column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
-
19
relation.select_values = [select_value]
-
-
19
query_builder = relation.arel
-
19
bind_values = query_builder.bind_values + relation.bind_values
-
end
-
-
19
result = @klass.connection.select_all(query_builder, nil, bind_values)
-
19
row = result.first
-
19
value = row && row.values.first
-
19
column = result.column_types.fetch(column_alias) do
-
19
type_for(column_name)
-
end
-
-
19
type_cast_calculated_value(value, column, operation)
-
end
-
-
2
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
-
group_attrs = group_values
-
-
if group_attrs.first.respond_to?(:to_sym)
-
association = @klass._reflect_on_association(group_attrs.first)
-
associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
-
group_fields = Array(associated ? association.foreign_key : group_attrs)
-
else
-
group_fields = group_attrs
-
end
-
-
group_aliases = group_fields.map { |field|
-
column_alias_for(field)
-
}
-
group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
-
[aliaz, field]
-
}
-
-
group = group_fields
-
-
if operation == 'count' && column_name == :all
-
aggregate_alias = 'count_all'
-
else
-
aggregate_alias = column_alias_for([operation, column_name].join(' '))
-
end
-
-
select_values = [
-
operation_over_aggregate_column(
-
aggregate_column(column_name),
-
operation,
-
distinct).as(aggregate_alias)
-
]
-
select_values += select_values unless having_values.empty?
-
-
select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
-
if field.respond_to?(:as)
-
field.as(aliaz)
-
else
-
"#{field} AS #{aliaz}"
-
end
-
}
-
-
relation = except(:group)
-
relation.group_values = group
-
relation.select_values = select_values
-
-
calculated_data = @klass.connection.select_all(relation, nil, relation.arel.bind_values + bind_values)
-
-
if association
-
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
-
key_records = association.klass.base_class.find(key_ids)
-
key_records = Hash[key_records.map { |r| [r.id, r] }]
-
end
-
-
Hash[calculated_data.map do |row|
-
key = group_columns.map { |aliaz, col_name|
-
column = calculated_data.column_types.fetch(aliaz) do
-
type_for(col_name)
-
end
-
type_cast_calculated_value(row[aliaz], column)
-
}
-
key = key.first if key.size == 1
-
key = key_records[key] if associated
-
-
column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
-
[key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
-
end]
-
end
-
-
# Converts the given keys to the value that the database adapter returns as
-
# a usable column name:
-
#
-
# column_alias_for("users.id") # => "users_id"
-
# column_alias_for("sum(id)") # => "sum_id"
-
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
-
# column_alias_for("count(*)") # => "count_all"
-
# column_alias_for("count", "id") # => "count_id"
-
2
def column_alias_for(keys)
-
if keys.respond_to? :name
-
keys = "#{keys.relation.name}.#{keys.name}"
-
end
-
-
table_name = keys.to_s.downcase
-
table_name.gsub!(/\*/, 'all')
-
table_name.gsub!(/\W+/, ' ')
-
table_name.strip!
-
table_name.gsub!(/ +/, '_')
-
-
@klass.connection.table_alias_for(table_name)
-
end
-
-
2
def type_for(field)
-
19
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
-
19
@klass.type_for_attribute(field_name)
-
end
-
-
2
def type_cast_calculated_value(value, type, operation = nil)
-
19
case operation
-
19
when 'count' then value.to_i
-
when 'sum' then type.type_cast_from_database(value || 0)
-
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
-
else type.type_cast_from_database(value)
-
end
-
end
-
-
# TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
-
2
def select_for_count
-
19
if select_values.present?
-
select_values.join(", ")
-
else
-
19
:all
-
end
-
end
-
-
2
def build_count_subquery(relation, column_name, distinct)
-
column_alias = Arel.sql('count_column')
-
subquery_alias = Arel.sql('subquery_for_count')
-
-
aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
-
relation.select_values = [aliased_column]
-
arel = relation.arel
-
subquery = arel.as(subquery_alias)
-
-
sm = Arel::SelectManager.new relation.engine
-
sm.bind_values = arel.bind_values
-
select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
-
sm.project(select_value).from(subquery)
-
end
-
end
-
end
-
2
require 'set'
-
2
require 'active_support/concern'
-
2
require 'active_support/deprecation'
-
-
2
module ActiveRecord
-
2
module Delegation # :nodoc:
-
2
module DelegateCache
-
2
def relation_delegate_class(klass) # :nodoc:
-
292
@relation_delegate_cache[klass]
-
end
-
-
2
def initialize_relation_delegate_cache # :nodoc:
-
12
@relation_delegate_cache = cache = {}
-
[
-
ActiveRecord::Relation,
-
ActiveRecord::Associations::CollectionProxy,
-
ActiveRecord::AssociationRelation
-
12
].each do |klass|
-
36
delegate = Class.new(klass) {
-
36
include ClassSpecificRelation
-
}
-
36
const_set klass.name.gsub('::', '_'), delegate
-
36
cache[klass] = delegate
-
end
-
end
-
-
2
def inherited(child_class)
-
12
child_class.initialize_relation_delegate_cache
-
12
super
-
end
-
end
-
-
2
extend ActiveSupport::Concern
-
-
# This module creates compiled delegation methods dynamically at runtime, which makes
-
# subsequent calls to that method faster by avoiding method_missing. The delegations
-
# may vary depending on the klass of a relation, so we create a subclass of Relation
-
# for each different klass, and the delegations are compiled into that subclass only.
-
-
2
BLACKLISTED_ARRAY_METHODS = [
-
:compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
-
:shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
-
:keep_if, :pop, :shift, :delete_at, :select!
-
].to_set # :nodoc:
-
-
2
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
-
-
2
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
-
:connection, :columns_hash, :to => :klass
-
-
2
module ClassSpecificRelation # :nodoc:
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
36
@delegation_mutex = Mutex.new
-
end
-
-
2
module ClassMethods # :nodoc:
-
2
def name
-
superclass.name
-
end
-
-
2
def delegate_to_scoped_klass(method)
-
6
@delegation_mutex.synchronize do
-
6
return if method_defined?(method)
-
-
6
if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
-
6
module_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{method}(*args, &block)
-
scoping { @klass.#{method}(*args, &block) }
-
end
-
RUBY
-
else
-
define_method method do |*args, &block|
-
scoping { @klass.public_send(method, *args, &block) }
-
end
-
end
-
end
-
end
-
-
2
def delegate(method, opts = {})
-
@delegation_mutex.synchronize do
-
return if method_defined?(method)
-
super
-
end
-
end
-
end
-
-
2
protected
-
-
2
def method_missing(method, *args, &block)
-
6
if @klass.respond_to?(method)
-
6
self.class.delegate_to_scoped_klass(method)
-
12
scoping { @klass.public_send(method, *args, &block) }
-
elsif arel.respond_to?(method)
-
self.class.delegate method, :to => :arel
-
arel.public_send(method, *args, &block)
-
else
-
super
-
end
-
end
-
end
-
-
2
module ClassMethods # :nodoc:
-
2
def create(klass, *args)
-
292
relation_class_for(klass).new(klass, *args)
-
end
-
-
2
private
-
-
2
def relation_class_for(klass)
-
292
klass.relation_delegate_class(self)
-
end
-
end
-
-
2
def respond_to?(method, include_private = false)
-
super || @klass.respond_to?(method, include_private) ||
-
array_delegable?(method) ||
-
arel.respond_to?(method, include_private)
-
end
-
-
2
protected
-
-
2
def array_delegable?(method)
-
Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
-
end
-
-
2
def method_missing(method, *args, &block)
-
if @klass.respond_to?(method)
-
scoping { @klass.public_send(method, *args, &block) }
-
elsif array_delegable?(method)
-
to_a.public_send(method, *args, &block)
-
elsif arel.respond_to?(method)
-
arel.public_send(method, *args, &block)
-
else
-
super
-
end
-
end
-
end
-
end
-
2
require 'active_support/deprecation'
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
2
module FinderMethods
-
2
ONE_AS_ONE = '1 AS one'
-
-
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
-
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
-
# is an integer, find by id coerces its arguments using +to_i+.
-
#
-
# Person.find(1) # returns the object for ID = 1
-
# Person.find("1") # returns the object for ID = 1
-
# Person.find("31-sarah") # returns the object for ID = 31
-
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
-
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
-
# Person.find([1]) # returns an array for the object with ID = 1
-
# Person.where("administrator = 1").order("created_on DESC").find(1)
-
#
-
# <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
-
#
-
# NOTE: The returned records may not be in the same order as the ids you
-
# provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
-
# option if you want the results are sorted.
-
#
-
# ==== Find with lock
-
#
-
# Example for find with a lock: Imagine two concurrent transactions:
-
# each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
-
# in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
-
# transaction has to wait until the first is finished; we get the
-
# expected <tt>person.visits == 4</tt>.
-
#
-
# Person.transaction do
-
# person = Person.lock(true).find(1)
-
# person.visits += 1
-
# person.save!
-
# end
-
#
-
# ==== Variations of +find+
-
#
-
# Person.where(name: 'Spartacus', rating: 4)
-
# # returns a chainable list (which can be empty).
-
#
-
# Person.find_by(name: 'Spartacus', rating: 4)
-
# # returns the first item or nil.
-
#
-
# Person.where(name: 'Spartacus', rating: 4).first_or_initialize
-
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
-
#
-
# Person.where(name: 'Spartacus', rating: 4).first_or_create
-
# # returns the first item or creates it and returns it, available since Rails 3.2.1.
-
#
-
# ==== Alternatives for +find+
-
#
-
# Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
-
# # returns a boolean indicating if any record with the given conditions exist.
-
#
-
# Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
-
# # returns a chainable list of instances with only the mentioned fields.
-
#
-
# Person.where(name: 'Spartacus', rating: 4).ids
-
# # returns an Array of ids, available since Rails 3.2.1.
-
#
-
# Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
-
# # returns an Array of the required fields, available since Rails 3.1.
-
2
def find(*args)
-
62
if block_given?
-
to_a.find(*args) { |*block_args| yield(*block_args) }
-
else
-
62
find_with_ids(*args)
-
end
-
end
-
-
# Finds the first record matching the specified conditions. There
-
# is no implied ordering so if order matters, you should specify it
-
# yourself.
-
#
-
# If no record is found, returns <tt>nil</tt>.
-
#
-
# Post.find_by name: 'Spartacus', rating: 4
-
# Post.find_by "published_at < ?", 2.weeks.ago
-
2
def find_by(*args)
-
where(*args).take
-
rescue RangeError
-
nil
-
end
-
-
# Like <tt>find_by</tt>, except that if no record is found, raises
-
# an <tt>ActiveRecord::RecordNotFound</tt> error.
-
2
def find_by!(*args)
-
where(*args).take!
-
rescue RangeError
-
raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value"
-
end
-
-
# Gives a record (or N records if a parameter is supplied) without any implied
-
# order. The order will depend on the database implementation.
-
# If an order is supplied it will be respected.
-
#
-
# Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
-
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
-
# Person.where(["name LIKE '%?'", name]).take
-
2
def take(limit = nil)
-
62
limit ? limit(limit).to_a : find_take
-
end
-
-
# Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found. Note that <tt>take!</tt> accepts no arguments.
-
2
def take!
-
take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql}]")
-
end
-
-
# Find the first record (or first N records if a parameter is supplied).
-
# If no order is defined it will order by primary key.
-
#
-
# Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1
-
# Person.where(["user_name = ?", user_name]).first
-
# Person.where(["user_name = :u", { u: user_name }]).first
-
# Person.order("created_on DESC").offset(5).first
-
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3
-
#
-
2
def first(limit = nil)
-
27
if limit
-
find_nth_with_limit(offset_index, limit)
-
else
-
27
find_nth(0, offset_index)
-
end
-
end
-
-
# Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found. Note that <tt>first!</tt> accepts no arguments.
-
2
def first!
-
find_nth! 0
-
end
-
-
# Find the last record (or last N records if a parameter is supplied).
-
# If no order is defined it will order by primary key.
-
#
-
# Person.last # returns the last object fetched by SELECT * FROM people
-
# Person.where(["user_name = ?", user_name]).last
-
# Person.order("created_on DESC").offset(5).last
-
# Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
-
#
-
# Take note that in that last case, the results are sorted in ascending order:
-
#
-
# [#<Person id:2>, #<Person id:3>, #<Person id:4>]
-
#
-
# and not:
-
#
-
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
-
2
def last(limit = nil)
-
if limit
-
if order_values.empty? && primary_key
-
order(arel_table[primary_key].desc).limit(limit).reverse
-
else
-
to_a.last(limit)
-
end
-
else
-
find_last
-
end
-
end
-
-
# Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found. Note that <tt>last!</tt> accepts no arguments.
-
2
def last!
-
last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql}]")
-
end
-
-
# Find the second record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.second # returns the second object fetched by SELECT * FROM people
-
# Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
-
# Person.where(["user_name = :u", { u: user_name }]).second
-
2
def second
-
find_nth(1, offset_index)
-
end
-
-
# Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found.
-
2
def second!
-
find_nth! 1
-
end
-
-
# Find the third record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.third # returns the third object fetched by SELECT * FROM people
-
# Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
-
# Person.where(["user_name = :u", { u: user_name }]).third
-
2
def third
-
find_nth(2, offset_index)
-
end
-
-
# Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found.
-
2
def third!
-
find_nth! 2
-
end
-
-
# Find the fourth record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.fourth # returns the fourth object fetched by SELECT * FROM people
-
# Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
-
# Person.where(["user_name = :u", { u: user_name }]).fourth
-
2
def fourth
-
find_nth(3, offset_index)
-
end
-
-
# Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found.
-
2
def fourth!
-
find_nth! 3
-
end
-
-
# Find the fifth record.
-
# If no order is defined it will order by primary key.
-
#
-
# Person.fifth # returns the fifth object fetched by SELECT * FROM people
-
# Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
-
# Person.where(["user_name = :u", { u: user_name }]).fifth
-
2
def fifth
-
find_nth(4, offset_index)
-
end
-
-
# Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found.
-
2
def fifth!
-
find_nth! 4
-
end
-
-
# Find the forty-second record. Also known as accessing "the reddit".
-
# If no order is defined it will order by primary key.
-
#
-
# Person.forty_two # returns the forty-second object fetched by SELECT * FROM people
-
# Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
-
# Person.where(["user_name = :u", { u: user_name }]).forty_two
-
2
def forty_two
-
find_nth(41, offset_index)
-
end
-
-
# Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
-
# is found.
-
2
def forty_two!
-
find_nth! 41
-
end
-
-
# Returns +true+ if a record exists in the table that matches the +id+ or
-
# conditions given, or +false+ otherwise. The argument can take six forms:
-
#
-
# * Integer - Finds the record with this primary key.
-
# * String - Finds the record with a primary key corresponding to this
-
# string (such as <tt>'5'</tt>).
-
# * Array - Finds the record that matches these +find+-style conditions
-
# (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
-
# * Hash - Finds the record that matches these +find+-style conditions
-
# (such as <tt>{name: 'David'}</tt>).
-
# * +false+ - Returns always +false+.
-
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
-
#
-
# For more information about specifying conditions as a hash or array,
-
# see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
-
#
-
# Note: You can't pass in a condition as a string (like <tt>name =
-
# 'Jamie'</tt>), since it would be sanitized and then queried against
-
# the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
-
#
-
# Person.exists?(5)
-
# Person.exists?('5')
-
# Person.exists?(['name LIKE ?', "%#{query}%"])
-
# Person.exists?(id: [1, 4, 8])
-
# Person.exists?(name: 'David')
-
# Person.exists?(false)
-
# Person.exists?
-
2
def exists?(conditions = :none)
-
15
if Base === conditions
-
conditions = conditions.id
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
You are passing an instance of ActiveRecord::Base to `exists?`.
-
Please pass the id of the object by calling `.id`
-
MSG
-
end
-
-
15
return false if !conditions
-
-
15
relation = apply_join_dependency(self, construct_join_dependency)
-
15
return false if ActiveRecord::NullRelation === relation
-
-
15
relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
-
-
15
case conditions
-
when Array, Hash
-
relation = relation.where(conditions)
-
else
-
15
unless conditions == :none
-
relation = relation.where(primary_key => conditions)
-
end
-
end
-
-
15
connection.select_value(relation, "#{name} Exists", relation.arel.bind_values + relation.bind_values) ? true : false
-
end
-
-
# This method is called whenever no records are found with either a single
-
# id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
-
#
-
# The error message is different depending on whether a single id or
-
# multiple ids are provided. If multiple ids are provided, then the number
-
# of results obtained should be provided in the +result_size+ argument and
-
# the expected number of results should be provided in the +expected_size+
-
# argument.
-
2
def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
-
4
conditions = arel.where_sql
-
4
conditions = " [#{conditions}]" if conditions
-
-
4
if Array(ids).size == 1
-
4
error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}"
-
else
-
error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': "
-
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
-
end
-
-
4
raise RecordNotFound, error
-
end
-
-
2
private
-
-
2
def offset_index
-
27
offset_value || 0
-
end
-
-
2
def find_with_associations
-
# NOTE: the JoinDependency constructed here needs to know about
-
# any joins already present in `self`, so pass them in
-
#
-
# failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
-
# incorrect SQL is generated. In that case, the join dependency for
-
# SpecialCategorizations is constructed without knowledge of the
-
# preexisting join in joins_values to categorizations (by way of
-
# the `has_many :through` for categories).
-
#
-
join_dependency = construct_join_dependency(joins_values)
-
-
aliases = join_dependency.aliases
-
relation = select aliases.columns
-
relation = apply_join_dependency(relation, join_dependency)
-
-
if block_given?
-
yield relation
-
else
-
if ActiveRecord::NullRelation === relation
-
[]
-
else
-
arel = relation.arel
-
rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
-
join_dependency.instantiate(rows, aliases)
-
end
-
end
-
end
-
-
2
def construct_join_dependency(joins = [])
-
15
including = eager_load_values + includes_values
-
15
ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
-
end
-
-
2
def construct_relation_for_association_calculations
-
from = arel.froms.first
-
if Arel::Table === from
-
apply_join_dependency(self, construct_join_dependency(joins_values))
-
else
-
# FIXME: as far as I can tell, `from` will always be an Arel::Table.
-
# There are no tests that test this branch, but presumably it's
-
# possible for `from` to be a list?
-
apply_join_dependency(self, construct_join_dependency(from))
-
end
-
end
-
-
2
def apply_join_dependency(relation, join_dependency)
-
15
relation = relation.except(:includes, :eager_load, :preload)
-
15
relation = relation.joins join_dependency
-
-
15
if using_limitable_reflections?(join_dependency.reflections)
-
15
relation
-
else
-
if relation.limit_value
-
limited_ids = limited_ids_for(relation)
-
limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids))
-
end
-
relation.except(:limit, :offset)
-
end
-
end
-
-
2
def limited_ids_for(relation)
-
values = @klass.connection.columns_for_distinct(
-
"#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
-
-
relation = relation.except(:select).select(values).distinct!
-
arel = relation.arel
-
-
id_rows = @klass.connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
-
id_rows.map {|row| row[primary_key]}
-
end
-
-
2
def using_limitable_reflections?(reflections)
-
15
reflections.none? { |r| r.collection? }
-
end
-
-
2
protected
-
-
2
def find_with_ids(*ids)
-
62
raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
-
-
62
expects_array = ids.first.kind_of?(Array)
-
62
return ids.first if expects_array && ids.first.empty?
-
-
62
ids = ids.flatten.compact.uniq
-
-
62
case ids.size
-
when 0
-
raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
-
when 1
-
62
result = find_one(ids.first)
-
58
expects_array ? [ result ] : result
-
else
-
find_some(ids)
-
end
-
rescue RangeError
-
raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
-
end
-
-
2
def find_one(id)
-
62
if ActiveRecord::Base === id
-
id = id.id
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
You are passing an instance of ActiveRecord::Base to `find`.
-
Please pass the id of the object by calling `.id`
-
MSG
-
end
-
-
62
relation = where(primary_key => id)
-
62
record = relation.take
-
-
62
raise_record_not_found_exception!(id, 0, 1) unless record
-
-
58
record
-
end
-
-
2
def find_some(ids)
-
result = where(primary_key => ids).to_a
-
-
expected_size =
-
if limit_value && ids.size > limit_value
-
limit_value
-
else
-
ids.size
-
end
-
-
# 11 ids with limit 3, offset 9 should give 2 results.
-
if offset_value && (ids.size - offset_value < expected_size)
-
expected_size = ids.size - offset_value
-
end
-
-
if result.size == expected_size
-
result
-
else
-
raise_record_not_found_exception!(ids, result.size, expected_size)
-
end
-
end
-
-
2
def find_take
-
62
if loaded?
-
@records.first
-
else
-
62
@take ||= limit(1).to_a.first
-
end
-
end
-
-
2
def find_nth(index, offset)
-
27
if loaded?
-
@records[index]
-
else
-
27
offset += index
-
27
@offsets[offset] ||= find_nth_with_limit(offset, 1).first
-
end
-
end
-
-
2
def find_nth!(index)
-
find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql}]")
-
end
-
-
2
def find_nth_with_limit(offset, limit)
-
27
relation = if order_values.empty? && primary_key
-
27
order(arel_table[primary_key].asc)
-
else
-
self
-
end
-
-
27
relation = relation.offset(offset) unless offset.zero?
-
27
relation.limit(limit).to_a
-
end
-
-
2
def find_last
-
if loaded?
-
@records.last
-
else
-
@last ||=
-
if limit_value
-
to_a.last
-
else
-
reverse_order.limit(1).to_a.first
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/keys'
-
2
require "set"
-
-
2
module ActiveRecord
-
2
class Relation
-
2
class HashMerger # :nodoc:
-
2
attr_reader :relation, :hash
-
-
2
def initialize(relation, hash)
-
hash.assert_valid_keys(*Relation::VALUE_METHODS)
-
-
@relation = relation
-
@hash = hash
-
end
-
-
2
def merge #:nodoc:
-
Merger.new(relation, other).merge
-
end
-
-
# Applying values to a relation has some side effects. E.g.
-
# interpolation might take place for where values. So we should
-
# build a relation to merge in rather than directly merging
-
# the values.
-
2
def other
-
other = Relation.create(relation.klass, relation.table)
-
hash.each { |k, v|
-
if k == :joins
-
if Hash === v
-
other.joins!(v)
-
else
-
other.joins!(*v)
-
end
-
elsif k == :select
-
other._select!(v)
-
else
-
other.send("#{k}!", v)
-
end
-
}
-
other
-
end
-
end
-
-
2
class Merger # :nodoc:
-
2
attr_reader :relation, :values, :other
-
-
2
def initialize(relation, other)
-
25
@relation = relation
-
25
@values = other.values
-
25
@other = other
-
end
-
-
2
NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
-
Relation::MULTI_VALUE_METHODS -
-
[:includes, :preload, :joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
-
-
-
2
def normal_values
-
25
NORMAL_VALUES
-
end
-
-
2
def merge
-
25
normal_values.each do |name|
-
300
value = values[name]
-
# The unless clause is here mostly for performance reasons (since the `send` call might be moderately
-
# expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
-
# `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
-
# don't fall through the cracks.
-
300
unless value.nil? || (value.blank? && false != value)
-
if name == :select
-
relation._select!(*value)
-
else
-
relation.send("#{name}!", *value)
-
end
-
end
-
end
-
-
25
merge_multi_values
-
25
merge_single_values
-
25
merge_preloads
-
25
merge_joins
-
-
25
relation
-
end
-
-
2
private
-
-
2
def merge_preloads
-
25
return if other.preload_values.empty? && other.includes_values.empty?
-
-
if other.klass == relation.klass
-
relation.preload!(*other.preload_values) unless other.preload_values.empty?
-
relation.includes!(other.includes_values) unless other.includes_values.empty?
-
else
-
reflection = relation.klass.reflect_on_all_associations.find do |r|
-
r.class_name == other.klass.name
-
end || return
-
-
unless other.preload_values.empty?
-
relation.preload! reflection.name => other.preload_values
-
end
-
-
unless other.includes_values.empty?
-
relation.includes! reflection.name => other.includes_values
-
end
-
end
-
end
-
-
2
def merge_joins
-
25
return if other.joins_values.blank?
-
-
if other.klass == relation.klass
-
relation.joins!(*other.joins_values)
-
else
-
joins_dependency, rest = other.joins_values.partition do |join|
-
case join
-
when Hash, Symbol, Array
-
true
-
else
-
false
-
end
-
end
-
-
join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
-
joins_dependency,
-
[])
-
relation.joins! rest
-
-
@relation = relation.joins join_dependency
-
end
-
end
-
-
2
def merge_multi_values
-
25
lhs_wheres = relation.where_values
-
25
rhs_wheres = other.where_values
-
-
25
lhs_binds = relation.bind_values
-
25
rhs_binds = other.bind_values
-
-
25
removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
-
-
25
where_values = kept + rhs_wheres
-
25
bind_values = filter_binds(lhs_binds, removed) + rhs_binds
-
-
25
relation.where_values = where_values
-
25
relation.bind_values = bind_values
-
-
25
if other.reordering_value
-
# override any order specified in the original relation
-
relation.reorder! other.order_values
-
elsif other.order_values
-
# merge in order_values from relation
-
25
relation.order! other.order_values
-
end
-
-
25
relation.extend(*other.extending_values) unless other.extending_values.blank?
-
end
-
-
2
def merge_single_values
-
25
relation.from_value = other.from_value unless relation.from_value
-
25
relation.lock_value = other.lock_value unless relation.lock_value
-
-
25
unless other.create_with_value.blank?
-
relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
-
end
-
end
-
-
2
def filter_binds(lhs_binds, removed_wheres)
-
25
return lhs_binds if removed_wheres.empty?
-
-
set = Set.new removed_wheres.map { |x| x.left.name.to_s }
-
lhs_binds.dup.delete_if { |col,_| set.include? col.name }
-
end
-
-
# Remove equalities from the existing relation with a LHS which is
-
# present in the relation being merged in.
-
# returns [things_to_remove, things_to_keep]
-
2
def partition_overwrites(lhs_wheres, rhs_wheres)
-
25
if lhs_wheres.empty? || rhs_wheres.empty?
-
25
return [[], lhs_wheres]
-
end
-
-
nodes = rhs_wheres.find_all do |w|
-
w.respond_to?(:operator) && w.operator == :==
-
end
-
seen = Set.new(nodes) { |node| node.left }
-
-
lhs_wheres.partition do |w|
-
w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
class PredicateBuilder # :nodoc:
-
2
@handlers = []
-
-
2
autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
-
2
autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
-
-
2
def self.resolve_column_aliases(klass, hash)
-
# This method is a hot spot, so for now, use Hash[] to dup the hash.
-
# https://bugs.ruby-lang.org/issues/7166
-
101
hash = Hash[hash]
-
101
hash.keys.grep(Symbol) do |key|
-
7
if klass.attribute_alias? key
-
hash[klass.attribute_alias(key)] = hash.delete key
-
end
-
end
-
101
hash
-
end
-
-
2
def self.build_from_hash(klass, attributes, default_table)
-
101
queries = []
-
-
101
attributes.each do |column, value|
-
101
table = default_table
-
-
101
if value.is_a?(Hash)
-
if value.empty?
-
queries << '1=0'
-
else
-
table = Arel::Table.new(column, default_table.engine)
-
association = klass._reflect_on_association(column)
-
-
value.each do |k, v|
-
queries.concat expand(association && association.klass, table, k, v)
-
end
-
end
-
else
-
101
column = column.to_s
-
-
101
if column.include?('.')
-
table_name, column = column.split('.', 2)
-
table = Arel::Table.new(table_name, default_table.engine)
-
end
-
-
101
queries.concat expand(klass, table, column, value)
-
end
-
end
-
-
101
queries
-
end
-
-
2
def self.expand(klass, table, column, value)
-
101
queries = []
-
-
# Find the foreign key when using queries such as:
-
# Post.where(author: author)
-
#
-
# For polymorphic relationships, find the foreign key and type:
-
# PriceEstimate.where(estimate_of: treasure)
-
101
if klass && reflection = klass._reflect_on_association(column)
-
base_class = polymorphic_base_class_from_value(value)
-
-
if reflection.polymorphic? && base_class
-
queries << build(table[reflection.foreign_type], base_class)
-
end
-
-
column = reflection.foreign_key
-
-
if base_class
-
primary_key = reflection.association_primary_key(base_class)
-
value = convert_value_to_association_ids(value, primary_key)
-
end
-
end
-
-
101
queries << build(table[column], value)
-
101
queries
-
end
-
-
2
def self.polymorphic_base_class_from_value(value)
-
case value
-
when Relation
-
value.klass.base_class
-
when Array
-
val = value.compact.first
-
val.class.base_class if val.is_a?(Base)
-
when Base
-
value.class.base_class
-
end
-
end
-
-
2
def self.references(attributes)
-
attributes.map do |key, value|
-
101
if value.is_a?(Hash)
-
key
-
else
-
101
key = key.to_s
-
101
key.split('.').first if key.include?('.')
-
end
-
101
end.compact
-
end
-
-
# Define how a class is converted to Arel nodes when passed to +where+.
-
# The handler can be any object that responds to +call+, and will be used
-
# for any value that +===+ the class given. For example:
-
#
-
# MyCustomDateRange = Struct.new(:start, :end)
-
# handler = proc do |column, range|
-
# Arel::Nodes::Between.new(column,
-
# Arel::Nodes::And.new([range.start, range.end])
-
# )
-
# end
-
# ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
-
2
def self.register_handler(klass, handler)
-
12
@handlers.unshift([klass, handler])
-
end
-
-
101
BASIC_OBJECT_HANDLER = ->(attribute, value) { attribute.eq(value) } # :nodoc:
-
2
register_handler(BasicObject, BASIC_OBJECT_HANDLER)
-
# FIXME: I think we need to deprecate this behavior
-
2
register_handler(Class, ->(attribute, value) { attribute.eq(value.name) })
-
2
register_handler(Base, ->(attribute, value) { attribute.eq(value.id) })
-
2
register_handler(Range, ->(attribute, value) { attribute.between(value) })
-
2
register_handler(Relation, RelationHandler.new)
-
2
register_handler(Array, ArrayHandler.new)
-
-
2
def self.build(attribute, value)
-
101
handler_for(value).call(attribute, value)
-
end
-
2
private_class_method :build
-
-
2
def self.handler_for(object)
-
1394
@handlers.detect { |klass, _| klass === object }.last
-
end
-
2
private_class_method :handler_for
-
-
2
def self.convert_value_to_association_ids(value, primary_key)
-
case value
-
when Relation
-
value.select(primary_key)
-
when Array
-
value.map { |v| convert_value_to_association_ids(v, primary_key) }
-
when Base
-
value._read_attribute(primary_key)
-
else
-
value
-
end
-
end
-
-
2
def self.can_be_bound?(value) # :nodoc:
-
!value.nil? &&
-
101
!value.is_a?(Hash) &&
-
handler_for(value) == BASIC_OBJECT_HANDLER
-
end
-
end
-
end
-
2
require 'active_support/core_ext/string/filters'
-
-
2
module ActiveRecord
-
2
class PredicateBuilder
-
2
class ArrayHandler # :nodoc:
-
2
def call(attribute, value)
-
4
values = value.map { |x| x.is_a?(Base) ? x.id : x }
-
2
nils, values = values.partition(&:nil?)
-
-
4
if values.any? { |val| val.is_a?(Array) }
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
Passing a nested array to Active Record finder methods is
-
deprecated and will be removed. Flatten your array before using
-
it for 'IN' conditions.
-
MSG
-
-
values = values.flatten
-
end
-
-
2
return attribute.in([]) if values.empty? && nils.empty?
-
-
4
ranges, values = values.partition { |v| v.is_a?(Range) }
-
-
2
values_predicate =
-
case values.length
-
when 0 then NullPredicate
-
2
when 1 then attribute.eq(values.first)
-
else attribute.in(values)
-
end
-
-
2
unless nils.empty?
-
values_predicate = values_predicate.or(attribute.eq(nil))
-
end
-
-
2
array_predicates = ranges.map { |range| attribute.between(range) }
-
2
array_predicates.unshift(values_predicate)
-
2
array_predicates.inject { |composite, predicate| composite.or(predicate) }
-
end
-
-
2
module NullPredicate # :nodoc:
-
2
def self.or(other)
-
other
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
class PredicateBuilder
-
2
class RelationHandler # :nodoc:
-
2
def call(attribute, value)
-
if value.select_values.empty?
-
value = value.select(value.klass.arel_table[value.klass.primary_key])
-
end
-
-
attribute.in(value.arel)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/array/wrap'
-
2
require 'active_support/core_ext/string/filters'
-
2
require 'active_model/forbidden_attributes_protection'
-
-
2
module ActiveRecord
-
2
module QueryMethods
-
2
extend ActiveSupport::Concern
-
-
2
include ActiveModel::ForbiddenAttributesProtection
-
-
# WhereChain objects act as placeholder for queries in which #where does not have any parameter.
-
# In this case, #where must be chained with #not to return a new relation.
-
2
class WhereChain
-
2
def initialize(scope)
-
@scope = scope
-
end
-
-
# Returns a new relation expressing WHERE + NOT condition according to
-
# the conditions in the arguments.
-
#
-
# +not+ accepts conditions as a string, array, or hash. See #where for
-
# more details on each format.
-
#
-
# User.where.not("name = 'Jon'")
-
# # SELECT * FROM users WHERE NOT (name = 'Jon')
-
#
-
# User.where.not(["name = ?", "Jon"])
-
# # SELECT * FROM users WHERE NOT (name = 'Jon')
-
#
-
# User.where.not(name: "Jon")
-
# # SELECT * FROM users WHERE name != 'Jon'
-
#
-
# User.where.not(name: nil)
-
# # SELECT * FROM users WHERE name IS NOT NULL
-
#
-
# User.where.not(name: %w(Ko1 Nobu))
-
# # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
-
#
-
# User.where.not(name: "Jon", role: "admin")
-
# # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
-
2
def not(opts, *rest)
-
where_value = @scope.send(:build_where, opts, rest).map do |rel|
-
case rel
-
when NilClass
-
raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
-
when Arel::Nodes::In
-
Arel::Nodes::NotIn.new(rel.left, rel.right)
-
when Arel::Nodes::Equality
-
Arel::Nodes::NotEqual.new(rel.left, rel.right)
-
when String
-
Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel))
-
else
-
Arel::Nodes::Not.new(rel)
-
end
-
end
-
-
@scope.references!(PredicateBuilder.references(opts)) if Hash === opts
-
@scope.where_values += where_value
-
@scope
-
end
-
end
-
-
2
Relation::MULTI_VALUE_METHODS.each do |name|
-
26
class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name}_values # def select_values
-
@values[:#{name}] || [] # @values[:select] || []
-
end # end
-
#
-
def #{name}_values=(values) # def select_values=(values)
-
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
-
check_cached_relation
-
@values[:#{name}] = values # @values[:select] = values
-
end # end
-
CODE
-
end
-
-
2
(Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name|
-
18
class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name}_value # def readonly_value
-
@values[:#{name}] # @values[:readonly]
-
end # end
-
CODE
-
end
-
-
2
Relation::SINGLE_VALUE_METHODS.each do |name|
-
20
class_eval <<-CODE, __FILE__, __LINE__ + 1
-
def #{name}_value=(value) # def readonly_value=(value)
-
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
-
check_cached_relation
-
@values[:#{name}] = value # @values[:readonly] = value
-
end # end
-
CODE
-
end
-
-
2
def check_cached_relation # :nodoc:
-
702
if defined?(@arel) && @arel
-
@arel = nil
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
-
Modifying already cached Relation. The cache will be reset. Use a
-
cloned Relation to prevent this warning.
-
MSG
-
end
-
end
-
-
2
def create_with_value # :nodoc:
-
25
@values[:create_with] || {}
-
end
-
-
2
alias extensions extending_values
-
-
# Specify relationships to be included in the result set. For
-
# example:
-
#
-
# users = User.includes(:address)
-
# users.each do |user|
-
# user.address.city
-
# end
-
#
-
# allows you to access the +address+ attribute of the +User+ model without
-
# firing an additional query. This will often result in a
-
# performance improvement over a simple +join+.
-
#
-
# You can also specify multiple relationships, like this:
-
#
-
# users = User.includes(:address, :friends)
-
#
-
# Loading nested relationships is possible using a Hash:
-
#
-
# users = User.includes(:address, friends: [:address, :followers])
-
#
-
# === conditions
-
#
-
# If you want to add conditions to your included models you'll have
-
# to explicitly reference them. For example:
-
#
-
# User.includes(:posts).where('posts.name = ?', 'example')
-
#
-
# Will throw an error, but this will work:
-
#
-
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
-
#
-
# Note that +includes+ works with association names while +references+ needs
-
# the actual table name.
-
2
def includes(*args)
-
check_if_method_has_arguments!(:includes, args)
-
spawn.includes!(*args)
-
end
-
-
2
def includes!(*args) # :nodoc:
-
args.reject!(&:blank?)
-
args.flatten!
-
-
self.includes_values |= args
-
self
-
end
-
-
# Forces eager loading by performing a LEFT OUTER JOIN on +args+:
-
#
-
# User.eager_load(:posts)
-
# => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ...
-
# FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
-
# "users"."id"
-
2
def eager_load(*args)
-
check_if_method_has_arguments!(:eager_load, args)
-
spawn.eager_load!(*args)
-
end
-
-
2
def eager_load!(*args) # :nodoc:
-
self.eager_load_values += args
-
self
-
end
-
-
# Allows preloading of +args+, in the same way that +includes+ does:
-
#
-
# User.preload(:posts)
-
# => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
-
2
def preload(*args)
-
check_if_method_has_arguments!(:preload, args)
-
spawn.preload!(*args)
-
end
-
-
2
def preload!(*args) # :nodoc:
-
self.preload_values += args
-
self
-
end
-
-
# Use to indicate that the given +table_names+ are referenced by an SQL string,
-
# and should therefore be JOINed in any query rather than loaded separately.
-
# This method only works in conjunction with +includes+.
-
# See #includes for more details.
-
#
-
# User.includes(:posts).where("posts.name = 'foo'")
-
# # => Doesn't JOIN the posts table, resulting in an error.
-
#
-
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
-
# # => Query now knows the string references posts, so adds a JOIN
-
2
def references(*table_names)
-
check_if_method_has_arguments!(:references, table_names)
-
spawn.references!(*table_names)
-
end
-
-
2
def references!(*table_names) # :nodoc:
-
101
table_names.flatten!
-
101
table_names.map!(&:to_s)
-
-
101
self.references_values |= table_names
-
101
self
-
end
-
-
# Works in two unique ways.
-
#
-
# First: takes a block so it can be used just like Array#select.
-
#
-
# Model.all.select { |m| m.field == value }
-
#
-
# This will build an array of objects from the database for the scope,
-
# converting them into an array and iterating through them using Array#select.
-
#
-
# Second: Modifies the SELECT statement for the query so that only certain
-
# fields are retrieved:
-
#
-
# Model.select(:field)
-
# # => [#<Model id: nil, field: "value">]
-
#
-
# Although in the above example it looks as though this method returns an
-
# array, it actually returns a relation object and can have other query
-
# methods appended to it, such as the other methods in ActiveRecord::QueryMethods.
-
#
-
# The argument to the method can also be an array of fields.
-
#
-
# Model.select(:field, :other_field, :and_one_more)
-
# # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
-
#
-
# You can also use one or more strings, which will be used unchanged as SELECT fields.
-
#
-
# Model.select('field AS field_one', 'other_field AS field_two')
-
# # => [#<Model id: nil, field: "value", other_field: "value">]
-
#
-
# If an alias was specified, it will be accessible from the resulting objects:
-
#
-
# Model.select('field AS field_one').first.field_one
-
# # => "value"
-
#
-
# Accessing attributes of an object that do not have fields retrieved by a select
-
# except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>:
-
#
-
# Model.select(:field).first.other_field
-
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
-
2
def select(*fields)
-
15
if block_given?
-
to_a.select { |*block_args| yield(*block_args) }
-
else
-
15
raise ArgumentError, 'Call this with at least one field' if fields.empty?
-
15
spawn._select!(*fields)
-
end
-
end
-
-
2
def _select!(*fields) # :nodoc:
-
15
fields.flatten!
-
15
fields.map! do |field|
-
15
klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
-
end
-
15
self.select_values += fields
-
15
self
-
end
-
-
# Allows to specify a group attribute:
-
#
-
# User.group(:name)
-
# => SELECT "users".* FROM "users" GROUP BY name
-
#
-
# Returns an array with distinct records based on the +group+ attribute:
-
#
-
# User.select([:id, :name])
-
# => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
-
#
-
# User.group(:name)
-
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
-
#
-
# User.group('name AS grouped_name, age')
-
# => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
-
#
-
# Passing in an array of attributes to group by is also supported.
-
# User.select([:id, :first_name]).group(:id, :first_name).first(3)
-
# => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
-
2
def group(*args)
-
check_if_method_has_arguments!(:group, args)
-
spawn.group!(*args)
-
end
-
-
2
def group!(*args) # :nodoc:
-
args.flatten!
-
-
self.group_values += args
-
self
-
end
-
-
# Allows to specify an order attribute:
-
#
-
# User.order(:name)
-
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
-
#
-
# User.order(email: :desc)
-
# => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
-
#
-
# User.order(:name, email: :desc)
-
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
-
#
-
# User.order('name')
-
# => SELECT "users".* FROM "users" ORDER BY name
-
#
-
# User.order('name DESC')
-
# => SELECT "users".* FROM "users" ORDER BY name DESC
-
#
-
# User.order('name DESC, email')
-
# => SELECT "users".* FROM "users" ORDER BY name DESC, email
-
2
def order(*args)
-
27
check_if_method_has_arguments!(:order, args)
-
27
spawn.order!(*args)
-
end
-
-
2
def order!(*args) # :nodoc:
-
52
preprocess_order_args(args)
-
-
52
self.order_values += args
-
52
self
-
end
-
-
# Replaces any existing order defined on the relation with the specified order.
-
#
-
# User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
-
#
-
# Subsequent calls to order on the same relation will be appended. For example:
-
#
-
# User.order('email DESC').reorder('id ASC').order('name ASC')
-
#
-
# generates a query with 'ORDER BY id ASC, name ASC'.
-
2
def reorder(*args)
-
check_if_method_has_arguments!(:reorder, args)
-
spawn.reorder!(*args)
-
end
-
-
2
def reorder!(*args) # :nodoc:
-
preprocess_order_args(args)
-
-
self.reordering_value = true
-
self.order_values = args
-
self
-
end
-
-
2
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
-
:limit, :offset, :joins, :includes, :from,
-
:readonly, :having])
-
-
# Removes an unwanted relation that is already defined on a chain of relations.
-
# This is useful when passing around chains of relations and would like to
-
# modify the relations without reconstructing the entire chain.
-
#
-
# User.order('email DESC').unscope(:order) == User.all
-
#
-
# The method arguments are symbols which correspond to the names of the methods
-
# which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES.
-
# The method can also be called with multiple arguments. For example:
-
#
-
# User.order('email DESC').select('id').where(name: "John")
-
# .unscope(:order, :select, :where) == User.all
-
#
-
# One can additionally pass a hash as an argument to unscope specific :where values.
-
# This is done by passing a hash with a single key-value pair. The key should be
-
# :where and the value should be the where value to unscope. For example:
-
#
-
# User.where(name: "John", active: true).unscope(where: :name)
-
# == User.where(active: true)
-
#
-
# This method is similar to <tt>except</tt>, but unlike
-
# <tt>except</tt>, it persists across merges:
-
#
-
# User.order('email').merge(User.except(:order))
-
# == User.order('email')
-
#
-
# User.order('email').merge(User.unscope(:order))
-
# == User.all
-
#
-
# This means it can be used in association definitions:
-
#
-
# has_many :comments, -> { unscope where: :trashed }
-
#
-
2
def unscope(*args)
-
19
check_if_method_has_arguments!(:unscope, args)
-
19
spawn.unscope!(*args)
-
end
-
-
2
def unscope!(*args) # :nodoc:
-
19
args.flatten!
-
19
self.unscope_values += args
-
-
19
args.each do |scope|
-
19
case scope
-
when Symbol
-
19
symbol_unscoping(scope)
-
when Hash
-
scope.each do |key, target_value|
-
if key != :where
-
raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
-
end
-
-
Array(target_value).each do |val|
-
where_unscoping(val)
-
end
-
end
-
else
-
raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
-
end
-
end
-
-
19
self
-
end
-
-
# Performs a joins on +args+:
-
#
-
# User.joins(:posts)
-
# => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
-
#
-
# You can use strings in order to customize your joins:
-
#
-
# User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
-
# => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
-
2
def joins(*args)
-
15
check_if_method_has_arguments!(:joins, args)
-
15
spawn.joins!(*args)
-
end
-
-
2
def joins!(*args) # :nodoc:
-
15
args.compact!
-
15
args.flatten!
-
15
self.joins_values += args
-
15
self
-
end
-
-
2
def bind(value) # :nodoc:
-
spawn.bind!(value)
-
end
-
-
2
def bind!(value) # :nodoc:
-
self.bind_values += [value]
-
self
-
end
-
-
# Returns a new relation, which is the result of filtering the current relation
-
# according to the conditions in the arguments.
-
#
-
# #where accepts conditions in one of several formats. In the examples below, the resulting
-
# SQL is given as an illustration; the actual query generated may be different depending
-
# on the database adapter.
-
#
-
# === string
-
#
-
# A single string, without additional arguments, is passed to the query
-
# constructor as an SQL fragment, and used in the where clause of the query.
-
#
-
# Client.where("orders_count = '2'")
-
# # SELECT * from clients where orders_count = '2';
-
#
-
# Note that building your own string from user input may expose your application
-
# to injection attacks if not done properly. As an alternative, it is recommended
-
# to use one of the following methods.
-
#
-
# === array
-
#
-
# If an array is passed, then the first element of the array is treated as a template, and
-
# the remaining elements are inserted into the template to generate the condition.
-
# Active Record takes care of building the query to avoid injection attacks, and will
-
# convert from the ruby type to the database type where needed. Elements are inserted
-
# into the string in the order in which they appear.
-
#
-
# User.where(["name = ? and email = ?", "Joe", "joe@example.com"])
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# Alternatively, you can use named placeholders in the template, and pass a hash as the
-
# second element of the array. The names in the template are replaced with the corresponding
-
# values from the hash.
-
#
-
# User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# This can make for more readable code in complex queries.
-
#
-
# Lastly, you can use sprintf-style % escapes in the template. This works slightly differently
-
# than the previous methods; you are responsible for ensuring that the values in the template
-
# are properly quoted. The values are passed to the connector for quoting, but the caller
-
# is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
-
# the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
-
#
-
# User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# If #where is called with multiple arguments, these are treated as if they were passed as
-
# the elements of a single array.
-
#
-
# User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
-
#
-
# When using strings to specify conditions, you can use any operator available from
-
# the database. While this provides the most flexibility, you can also unintentionally introduce
-
# dependencies on the underlying database. If your code is intended for general consumption,
-
# test with multiple database backends.
-
#
-
# === hash
-
#
-
# #where will also accept a hash condition, in which the keys are fields and the values
-
# are values to be searched for.
-
#
-
# Fields can be symbols or strings. Values can be single values, arrays, or ranges.
-
#
-
# User.where({ name: "Joe", email: "joe@example.com" })
-
# # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'
-
#
-
# User.where({ name: ["Alice", "Bob"]})
-
# # SELECT * FROM users WHERE name IN ('Alice', 'Bob')
-
#
-
# User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
-
# # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
-
#
-
# In the case of a belongs_to relationship, an association key can be used
-
# to specify the model if an ActiveRecord object is used as the value.
-
#
-
# author = Author.find(1)
-
#
-
# # The following queries will be equivalent:
-
# Post.where(author: author)
-
# Post.where(author_id: author)
-
#
-
# This also works with polymorphic belongs_to relationships:
-
#
-
# treasure = Treasure.create(name: 'gold coins')
-
# treasure.price_estimates << PriceEstimate.create(price: 125)
-
#
-
# # The following queries will be equivalent:
-
# PriceEstimate.where(estimate_of: treasure)
-
# PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
-
#
-
# === Joins
-
#
-
# If the relation is the result of a join, you may create a condition which uses any of the
-
# tables in the join. For string and array conditions, use the table name in the condition.
-
#
-
# User.joins(:posts).where("posts.created_at < ?", Time.now)
-
#
-
# For hash conditions, you can either use the table name in the key, or use a sub-hash.
-
#
-
# User.joins(:posts).where({ "posts.published" => true })
-
# User.joins(:posts).where({ posts: { published: true } })
-
#
-
# === no argument
-
#
-
# If no argument is passed, #where returns a new instance of WhereChain, that
-
# can be chained with #not to return a new relation that negates the where clause.
-
#
-
# User.where.not(name: "Jon")
-
# # SELECT * FROM users WHERE name != 'Jon'
-
#
-
# See WhereChain for more details on #not.
-
#
-
# === blank condition
-
#
-
# If the condition is any blank-ish object, then #where is a no-op and returns
-
# the current relation.
-
2
def where(opts = :chain, *rest)
-
127
if opts == :chain
-
WhereChain.new(spawn)
-
127
elsif opts.blank?
-
self
-
else
-
127
spawn.where!(opts, *rest)
-
end
-
end
-
-
2
def where!(opts, *rest) # :nodoc:
-
127
if Hash === opts
-
101
opts = sanitize_forbidden_attributes(opts)
-
101
references!(PredicateBuilder.references(opts))
-
end
-
-
127
self.where_values += build_where(opts, rest)
-
127
self
-
end
-
-
# Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
-
#
-
# Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0
-
# Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0
-
# Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0
-
#
-
# This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
-
# the named conditions -- not the entire where statement.
-
2
def rewhere(conditions)
-
unscope(where: conditions.keys).where(conditions)
-
end
-
-
# Allows to specify a HAVING clause. Note that you can't use HAVING
-
# without also specifying a GROUP clause.
-
#
-
# Order.having('SUM(price) > 30').group('user_id')
-
2
def having(opts, *rest)
-
opts.blank? ? self : spawn.having!(opts, *rest)
-
end
-
-
2
def having!(opts, *rest) # :nodoc:
-
references!(PredicateBuilder.references(opts)) if Hash === opts
-
-
self.having_values += build_where(opts, rest)
-
self
-
end
-
-
# Specifies a limit for the number of records to retrieve.
-
#
-
# User.limit(10) # generated SQL has 'LIMIT 10'
-
#
-
# User.limit(10).limit(20) # generated SQL has 'LIMIT 20'
-
2
def limit(value)
-
110
spawn.limit!(value)
-
end
-
-
2
def limit!(value) # :nodoc:
-
110
self.limit_value = value
-
110
self
-
end
-
-
# Specifies the number of rows to skip before returning rows.
-
#
-
# User.offset(10) # generated SQL has "OFFSET 10"
-
#
-
# Should be used with order.
-
#
-
# User.offset(10).order("name ASC")
-
2
def offset(value)
-
spawn.offset!(value)
-
end
-
-
2
def offset!(value) # :nodoc:
-
self.offset_value = value
-
self
-
end
-
-
# Specifies locking settings (default to +true+). For more information
-
# on locking, please see +ActiveRecord::Locking+.
-
2
def lock(locks = true)
-
spawn.lock!(locks)
-
end
-
-
2
def lock!(locks = true) # :nodoc:
-
case locks
-
when String, TrueClass, NilClass
-
self.lock_value = locks || true
-
else
-
self.lock_value = false
-
end
-
-
self
-
end
-
-
# Returns a chainable relation with zero records.
-
#
-
# The returned relation implements the Null Object pattern. It is an
-
# object with defined null behavior and always returns an empty array of
-
# records without querying the database.
-
#
-
# Any subsequent condition chained to the returned relation will continue
-
# generating an empty relation and will not fire any query to the database.
-
#
-
# Used in cases where a method or scope could return zero records but the
-
# result needs to be chainable.
-
#
-
# For example:
-
#
-
# @posts = current_user.visible_posts.where(name: params[:name])
-
# # => the visible_posts method is expected to return a chainable Relation
-
#
-
# def visible_posts
-
# case role
-
# when 'Country Manager'
-
# Post.where(country: country)
-
# when 'Reviewer'
-
# Post.published
-
# when 'Bad User'
-
# Post.none # It can't be chained if [] is returned.
-
# end
-
# end
-
#
-
2
def none
-
where("1=0").extending!(NullRelation)
-
end
-
-
2
def none! # :nodoc:
-
where!("1=0").extending!(NullRelation)
-
end
-
-
# Sets readonly attributes for the returned relation. If value is
-
# true (default), attempting to update a record will result in an error.
-
#
-
# users = User.readonly
-
# users.first.save
-
# => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
-
2
def readonly(value = true)
-
spawn.readonly!(value)
-
end
-
-
2
def readonly!(value = true) # :nodoc:
-
self.readonly_value = value
-
self
-
end
-
-
# Sets attributes to be used when creating new records from a
-
# relation object.
-
#
-
# users = User.where(name: 'Oscar')
-
# users.new.name # => 'Oscar'
-
#
-
# users = users.create_with(name: 'DHH')
-
# users.new.name # => 'DHH'
-
#
-
# You can pass +nil+ to +create_with+ to reset attributes:
-
#
-
# users = users.create_with(nil)
-
# users.new.name # => 'Oscar'
-
2
def create_with(value)
-
spawn.create_with!(value)
-
end
-
-
2
def create_with!(value) # :nodoc:
-
if value
-
value = sanitize_forbidden_attributes(value)
-
self.create_with_value = create_with_value.merge(value)
-
else
-
self.create_with_value = {}
-
end
-
-
self
-
end
-
-
# Specifies table from which the records will be fetched. For example:
-
#
-
# Topic.select('title').from('posts')
-
# # => SELECT title FROM posts
-
#
-
# Can accept other relation objects. For example:
-
#
-
# Topic.select('title').from(Topic.approved)
-
# # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
-
#
-
# Topic.select('a.title').from(Topic.approved, :a)
-
# # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
-
#
-
2
def from(value, subquery_name = nil)
-
spawn.from!(value, subquery_name)
-
end
-
-
2
def from!(value, subquery_name = nil) # :nodoc:
-
self.from_value = [value, subquery_name]
-
if value.is_a? Relation
-
self.bind_values = value.arel.bind_values + value.bind_values + bind_values
-
end
-
self
-
end
-
-
# Specifies whether the records should be unique or not. For example:
-
#
-
# User.select(:name)
-
# # => Might return two records with the same name
-
#
-
# User.select(:name).distinct
-
# # => Returns 1 record per distinct name
-
#
-
# User.select(:name).distinct.distinct(false)
-
# # => You can also remove the uniqueness
-
2
def distinct(value = true)
-
spawn.distinct!(value)
-
end
-
2
alias uniq distinct
-
-
# Like #distinct, but modifies relation in place.
-
2
def distinct!(value = true) # :nodoc:
-
self.distinct_value = value
-
self
-
end
-
2
alias uniq! distinct!
-
-
# Used to extend a scope with additional methods, either through
-
# a module or through a block provided.
-
#
-
# The object returned is a relation, which can be further extended.
-
#
-
# === Using a module
-
#
-
# module Pagination
-
# def page(number)
-
# # pagination code goes here
-
# end
-
# end
-
#
-
# scope = Model.all.extending(Pagination)
-
# scope.page(params[:page])
-
#
-
# You can also pass a list of modules:
-
#
-
# scope = Model.all.extending(Pagination, SomethingElse)
-
#
-
# === Using a block
-
#
-
# scope = Model.all.extending do
-
# def page(number)
-
# # pagination code goes here
-
# end
-
# end
-
# scope.page(params[:page])
-
#
-
# You can also use a block and a module list:
-
#
-
# scope = Model.all.extending(Pagination) do
-
# def per_page(number)
-
# # pagination code goes here
-
# end
-
# end
-
2
def extending(*modules, &block)
-
if modules.any? || block
-
spawn.extending!(*modules, &block)
-
else
-
self
-
end
-
end
-
-
2
def extending!(*modules, &block) # :nodoc:
-
8
modules << Module.new(&block) if block
-
8
modules.flatten!
-
-
8
self.extending_values += modules
-
8
extend(*extending_values) if extending_values.any?
-
-
8
self
-
end
-
-
# Reverse the existing order clause on the relation.
-
#
-
# User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
-
2
def reverse_order
-
spawn.reverse_order!
-
end
-
-
2
def reverse_order! # :nodoc:
-
orders = order_values.uniq
-
orders.reject!(&:blank?)
-
self.order_values = reverse_sql_order(orders)
-
self
-
end
-
-
# Returns the Arel object associated with the relation.
-
2
def arel # :nodoc:
-
297
@arel ||= build_arel
-
end
-
-
2
private
-
-
2
def build_arel
-
175
arel = Arel::SelectManager.new(table.engine, table)
-
-
175
build_joins(arel, joins_values.flatten) unless joins_values.empty?
-
-
175
collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
-
-
175
arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
-
-
175
arel.take(connection.sanitize_limit(limit_value)) if limit_value
-
175
arel.skip(offset_value.to_i) if offset_value
-
175
arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
-
-
175
build_order(arel)
-
-
175
build_select(arel)
-
-
175
arel.distinct(distinct_value)
-
175
arel.from(build_from) if from_value
-
175
arel.lock(lock_value) if lock_value
-
-
175
arel
-
end
-
-
2
def symbol_unscoping(scope)
-
19
if !VALID_UNSCOPING_VALUES.include?(scope)
-
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
-
end
-
-
19
single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
-
19
unscope_code = "#{scope}_value#{'s' unless single_val_method}="
-
-
19
case scope
-
when :order
-
19
result = []
-
when :where
-
self.bind_values = []
-
else
-
result = [] unless single_val_method
-
end
-
-
19
self.send(unscope_code, result)
-
end
-
-
2
def where_unscoping(target_value)
-
target_value = target_value.to_s
-
-
self.where_values = where_values.reject do |rel|
-
case rel
-
when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
-
subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
-
subrelation.name == target_value
-
end
-
end
-
-
bind_values.reject! { |col,_| col.name == target_value }
-
end
-
-
2
def custom_join_ast(table, joins)
-
15
joins = joins.reject(&:blank?)
-
-
15
return [] if joins.empty?
-
-
joins.map! do |join|
-
case join
-
when Array
-
join = Arel.sql(join.join(' ')) if array_of_strings?(join)
-
when String
-
join = Arel.sql(join)
-
end
-
table.create_string_join(join)
-
end
-
end
-
-
2
def collapse_wheres(arel, wheres)
-
175
predicates = wheres.map do |where|
-
125
next where if ::Arel::Nodes::Equality === where
-
4
where = Arel.sql(where) if String === where
-
4
Arel::Nodes::Grouping.new(where)
-
end
-
-
175
arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
-
end
-
-
2
def build_where(opts, other = [])
-
127
case opts
-
when String, Array
-
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
-
when Hash
-
101
opts = PredicateBuilder.resolve_column_aliases(klass, opts)
-
-
101
tmp_opts, bind_values = create_binds(opts)
-
101
self.bind_values += bind_values
-
-
101
attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
-
101
add_relations_to_bind_values(attributes)
-
-
101
PredicateBuilder.build_from_hash(klass, attributes, table)
-
else
-
26
[opts]
-
end
-
end
-
-
2
def create_binds(opts)
-
101
bindable, non_binds = opts.partition do |column, value|
-
PredicateBuilder.can_be_bound?(value) &&
-
101
@klass.columns_hash.include?(column.to_s) &&
-
!@klass.reflect_on_aggregation(column)
-
end
-
-
101
association_binds, non_binds = non_binds.partition do |column, value|
-
2
value.is_a?(Hash) && association_for_table(column)
-
end
-
-
101
new_opts = {}
-
101
binds = []
-
-
101
connection = self.connection
-
-
101
bindable.each do |(column,value)|
-
99
binds.push [@klass.columns_hash[column.to_s], value]
-
99
new_opts[column] = connection.substitute_at(column)
-
end
-
-
101
association_binds.each do |(column, value)|
-
association_relation = association_for_table(column).klass.send(:relation)
-
association_new_opts, association_bind = association_relation.send(:create_binds, value)
-
new_opts[column] = association_new_opts
-
binds += association_bind
-
end
-
-
103
non_binds.each { |column,value| new_opts[column] = value }
-
-
101
[new_opts, binds]
-
end
-
-
2
def association_for_table(table_name)
-
table_name = table_name.to_s
-
@klass._reflect_on_association(table_name) ||
-
@klass._reflect_on_association(table_name.singularize)
-
end
-
-
2
def build_from
-
opts, name = from_value
-
case opts
-
when Relation
-
name ||= 'subquery'
-
opts.arel.as(name.to_s)
-
else
-
opts
-
end
-
end
-
-
2
def build_joins(manager, joins)
-
15
buckets = joins.group_by do |join|
-
15
case join
-
when String
-
:string_join
-
when Hash, Symbol, Array
-
:association_join
-
when ActiveRecord::Associations::JoinDependency
-
15
:stashed_join
-
when Arel::Nodes::Join
-
:join_node
-
else
-
raise 'unknown class: %s' % join.class.name
-
end
-
end
-
-
15
association_joins = buckets[:association_join] || []
-
15
stashed_association_joins = buckets[:stashed_join] || []
-
15
join_nodes = (buckets[:join_node] || []).uniq
-
15
string_joins = (buckets[:string_join] || []).map(&:strip).uniq
-
-
15
join_list = join_nodes + custom_join_ast(manager, string_joins)
-
-
15
join_dependency = ActiveRecord::Associations::JoinDependency.new(
-
@klass,
-
association_joins,
-
join_list
-
)
-
-
15
join_infos = join_dependency.join_constraints stashed_association_joins
-
-
15
join_infos.each do |info|
-
info.joins.each { |join| manager.from(join) }
-
manager.bind_values.concat info.binds
-
end
-
-
15
manager.join_sources.concat(join_list)
-
-
15
manager
-
end
-
-
2
def build_select(arel)
-
175
if select_values.any?
-
39
arel.project(*arel_columns(select_values.uniq))
-
else
-
136
arel.project(@klass.arel_table[Arel.star])
-
end
-
end
-
-
2
def arel_columns(columns)
-
39
columns.map do |field|
-
39
if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_value
-
arel_table[field]
-
elsif Symbol === field
-
connection.quote_table_name(field.to_s)
-
else
-
39
field
-
end
-
end
-
end
-
-
2
def reverse_sql_order(order_query)
-
order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
-
-
order_query.flat_map do |o|
-
case o
-
when Arel::Nodes::Ordering
-
o.reverse
-
when String
-
o.to_s.split(',').map! do |s|
-
s.strip!
-
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
-
end
-
else
-
o
-
end
-
end
-
end
-
-
2
def array_of_strings?(o)
-
o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
-
end
-
-
2
def build_order(arel)
-
175
orders = order_values.uniq
-
175
orders.reject!(&:blank?)
-
-
175
arel.order(*orders) unless orders.empty?
-
end
-
-
2
VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
-
'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
-
-
2
def validate_order_args(args)
-
52
args.each do |arg|
-
27
next unless arg.is_a?(Hash)
-
arg.each do |_key, value|
-
raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
-
"directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
-
end
-
end
-
end
-
-
2
def preprocess_order_args(order_args)
-
52
order_args.flatten!
-
52
validate_order_args(order_args)
-
-
52
references = order_args.grep(String)
-
52
references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
-
52
references!(references) if references.any?
-
-
# if a symbol is given we prepend the quoted table name
-
order_args.map! do |arg|
-
27
case arg
-
when Symbol
-
arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
-
table[arg].asc
-
when Hash
-
arg.map { |field, dir|
-
field = klass.attribute_alias(field) if klass.attribute_alias?(field)
-
table[field].send(dir.downcase)
-
}
-
else
-
27
arg
-
end
-
52
end.flatten!
-
end
-
-
# Checks to make sure that the arguments are not blank. Note that if some
-
# blank-like object were initially passed into the query method, then this
-
# method will not raise an error.
-
#
-
# Example:
-
#
-
# Post.references() # => raises an error
-
# Post.references([]) # => does not raise an error
-
#
-
# This particular method should be called with a method_name and the args
-
# passed into that method as an input. For example:
-
#
-
# def references(*args)
-
# check_if_method_has_arguments!("references", args)
-
# ...
-
# end
-
2
def check_if_method_has_arguments!(method_name, args)
-
61
if args.blank?
-
raise ArgumentError, "The method .#{method_name}() must contain arguments."
-
end
-
end
-
-
2
def add_relations_to_bind_values(attributes)
-
202
if attributes.is_a?(Hash)
-
101
attributes.each_value do |value|
-
101
if value.is_a?(ActiveRecord::Relation)
-
self.bind_values += value.arel.bind_values + value.bind_values
-
else
-
101
add_relations_to_bind_values(value)
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/hash/slice'
-
2
require 'active_record/relation/merger'
-
-
2
module ActiveRecord
-
2
module SpawnMethods
-
-
# This is overridden by Associations::CollectionProxy
-
2
def spawn #:nodoc:
-
329
clone
-
end
-
-
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>.
-
# Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
-
#
-
# Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
-
# # Performs a single join query with both where conditions.
-
#
-
# recent_posts = Post.order('created_at DESC').first(5)
-
# Post.where(published: true).merge(recent_posts)
-
# # Returns the intersection of all published posts with the 5 most recently created posts.
-
# # (This is just an example. You'd probably want to do this with a single query!)
-
#
-
# Procs will be evaluated by merge:
-
#
-
# Post.where(published: true).merge(-> { joins(:comments) })
-
# # => Post.where(published: true).joins(:comments)
-
#
-
# This is mainly intended for sharing common conditions between multiple associations.
-
2
def merge(other)
-
81
if other.is_a?(Array)
-
to_a & other
-
81
elsif other
-
11
spawn.merge!(other)
-
else
-
70
self
-
end
-
end
-
-
2
def merge!(other) # :nodoc:
-
25
if other.is_a?(Hash)
-
Relation::HashMerger.new(self, other).merge
-
25
elsif other.is_a?(Relation)
-
25
Relation::Merger.new(self, other).merge
-
elsif other.respond_to?(:to_proc)
-
instance_exec(&other)
-
else
-
raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
-
end
-
end
-
-
# Removes from the query the condition(s) specified in +skips+.
-
#
-
# Post.order('id asc').except(:order) # discards the order condition
-
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
-
2
def except(*skips)
-
30
relation_with values.except(*skips)
-
end
-
-
# Removes any condition from the query other than the one(s) specified in +onlies+.
-
#
-
# Post.order('id asc').only(:where) # discards the order condition
-
# Post.order('id asc').only(:where, :order) # uses the specified order
-
2
def only(*onlies)
-
if onlies.any? { |o| o == :where }
-
onlies << :bind
-
end
-
relation_with values.slice(*onlies)
-
end
-
-
2
private
-
-
2
def relation_with(values) # :nodoc:
-
30
result = Relation.create(klass, table, values)
-
30
result.extend(*extending_values) if extending_values.any?
-
30
result
-
end
-
end
-
end
-
2
module ActiveRecord
-
###
-
# This class encapsulates a Result returned from calling +exec_query+ on any
-
# database connection adapter. For example:
-
#
-
# result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
-
# result # => #<ActiveRecord::Result:0xdeadbeef>
-
#
-
# # Get the column names of the result:
-
# result.columns
-
# # => ["id", "title", "body"]
-
#
-
# # Get the record values of the result:
-
# result.rows
-
# # => [[1, "title_1", "body_1"],
-
# [2, "title_2", "body_2"],
-
# ...
-
# ]
-
#
-
# # Get an array of hashes representing the result (column => value):
-
# result.to_hash
-
# # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
-
# {"id" => 2, "title" => "title_2", "body" => "body_2"},
-
# ...
-
# ]
-
#
-
# # ActiveRecord::Result also includes Enumerable.
-
# result.each do |row|
-
# puts row['title'] + " " + row['body']
-
# end
-
2
class Result
-
2
include Enumerable
-
-
2
IDENTITY_TYPE = Type::Value.new # :nodoc:
-
-
2
attr_reader :columns, :rows, :column_types
-
-
2
def initialize(columns, rows, column_types = {})
-
222
@columns = columns
-
222
@rows = rows
-
222
@hash_rows = nil
-
222
@column_types = column_types
-
end
-
-
2
def length
-
118
@rows.length
-
end
-
-
2
def each
-
156
if block_given?
-
357
hash_rows.each { |row| yield row }
-
else
-
hash_rows.to_enum { @rows.size }
-
end
-
end
-
-
2
def to_hash
-
32
hash_rows
-
end
-
-
2
alias :map! :map
-
2
alias :collect! :map
-
-
# Returns true if there are no records.
-
2
def empty?
-
rows.empty?
-
end
-
-
2
def to_ary
-
hash_rows
-
end
-
-
2
def [](idx)
-
hash_rows[idx]
-
end
-
-
2
def last
-
hash_rows.last
-
end
-
-
2
def cast_values(type_overrides = {}) # :nodoc:
-
10
types = columns.map { |name| column_type(name, type_overrides) }
-
5
result = rows.map do |values|
-
8
types.zip(values).map { |type, value| type.type_cast_from_database(value) }
-
end
-
-
5
columns.one? ? result.map!(&:first) : result
-
end
-
-
2
def initialize_copy(other)
-
@columns = columns.dup
-
@rows = rows.dup
-
@column_types = column_types.dup
-
@hash_rows = nil
-
end
-
-
2
private
-
-
2
def column_type(name, type_overrides = {})
-
5
type_overrides.fetch(name) do
-
column_types.fetch(name, IDENTITY_TYPE)
-
end
-
end
-
-
2
def hash_rows
-
@hash_rows ||=
-
begin
-
# We freeze the strings to prevent them getting duped when
-
# used as keys in ActiveRecord::Base's @attributes hash
-
1752
columns = @columns.map { |c| c.dup.freeze }
-
188
@rows.map { |row|
-
# In the past we used Hash[columns.zip(row)]
-
# though elegant, the verbose way is much more efficient
-
# both time and memory wise cause it avoids a big array allocation
-
# this method is called a lot and needs to be micro optimised
-
461
hash = {}
-
-
461
index = 0
-
461
length = columns.length
-
-
461
while index < length
-
2967
hash[columns[index]] = row[index]
-
2967
index += 1
-
end
-
-
461
hash
-
}
-
188
end
-
end
-
end
-
end
-
2
require 'active_support/per_thread_registry'
-
-
2
module ActiveRecord
-
# This is a thread locals registry for Active Record. For example:
-
#
-
# ActiveRecord::RuntimeRegistry.connection_handler
-
#
-
# returns the connection handler local to the current thread.
-
#
-
# See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
-
# for further details.
-
2
class RuntimeRegistry # :nodoc:
-
2
extend ActiveSupport::PerThreadRegistry
-
-
2
attr_accessor :connection_handler, :sql_runtime, :connection_id
-
-
2
[:connection_handler, :sql_runtime, :connection_id].each do |val|
-
6
class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
-
6
class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Sanitization
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
2
def quote_value(value, column) #:nodoc:
-
connection.quote(value, column)
-
end
-
-
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
-
2
def sanitize(object) #:nodoc:
-
connection.quote(object)
-
end
-
-
2
protected
-
-
# Accepts an array, hash, or string of SQL conditions and sanitizes
-
# them into a valid SQL fragment for a WHERE clause.
-
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
-
# { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'"
-
# "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
-
2
def sanitize_sql_for_conditions(condition, table_name = self.table_name)
-
118
return nil if condition.blank?
-
-
118
case condition
-
when Array; sanitize_sql_array(condition)
-
when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
-
118
else condition
-
end
-
end
-
2
alias_method :sanitize_sql, :sanitize_sql_for_conditions
-
2
alias_method :sanitize_conditions, :sanitize_sql
-
-
# Accepts an array, hash, or string of SQL conditions and sanitizes
-
# them into a valid SQL fragment for a SET clause.
-
# { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
-
2
def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
-
case assignments
-
when Array; sanitize_sql_array(assignments)
-
when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
-
else assignments
-
end
-
end
-
-
# Accepts a hash of SQL conditions and replaces those attributes
-
# that correspond to a +composed_of+ relationship with their expanded
-
# aggregate attribute values.
-
# Given:
-
# class Person < ActiveRecord::Base
-
# composed_of :address, class_name: "Address",
-
# mapping: [%w(address_street street), %w(address_city city)]
-
# end
-
# Then:
-
# { address: Address.new("813 abc st.", "chicago") }
-
# # => { address_street: "813 abc st.", address_city: "chicago" }
-
2
def expand_hash_conditions_for_aggregates(attrs)
-
101
expanded_attrs = {}
-
101
attrs.each do |attr, value|
-
101
if aggregation = reflect_on_aggregation(attr.to_sym)
-
mapping = aggregation.mapping
-
mapping.each do |field_attr, aggregate_attr|
-
if mapping.size == 1 && !value.respond_to?(aggregate_attr)
-
expanded_attrs[field_attr] = value
-
else
-
expanded_attrs[field_attr] = value.send(aggregate_attr)
-
end
-
end
-
else
-
101
expanded_attrs[attr] = value
-
end
-
end
-
101
expanded_attrs
-
end
-
-
# Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
-
# { name: "foo'bar", group_id: 4 }
-
# # => "name='foo''bar' and group_id= 4"
-
# { status: nil, group_id: [1,2,3] }
-
# # => "status IS NULL and group_id IN (1,2,3)"
-
# { age: 13..18 }
-
# # => "age BETWEEN 13 AND 18"
-
# { 'other_records.id' => 7 }
-
# # => "`other_records`.`id` = 7"
-
# { other_records: { id: 7 } }
-
# # => "`other_records`.`id` = 7"
-
# And for value objects on a composed_of relationship:
-
# { address: Address.new("123 abc st.", "chicago") }
-
# # => "address_street='123 abc st.' and address_city='chicago'"
-
2
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
-
ActiveSupport::Deprecation.warn(<<-EOWARN)
-
sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0
-
EOWARN
-
attrs = PredicateBuilder.resolve_column_aliases self, attrs
-
attrs = expand_hash_conditions_for_aggregates(attrs)
-
-
table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
-
PredicateBuilder.build_from_hash(self, attrs, table).map { |b|
-
connection.visitor.compile b
-
}.join(' AND ')
-
end
-
2
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
-
-
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
-
# { status: nil, group_id: 1 }
-
# # => "status = NULL , group_id = 1"
-
2
def sanitize_sql_hash_for_assignment(attrs, table)
-
c = connection
-
attrs.map do |attr, value|
-
"#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}"
-
end.join(', ')
-
end
-
-
# Sanitizes a +string+ so that it is safe to use within an SQL
-
# LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
-
2
def sanitize_sql_like(string, escape_character = "\\")
-
pattern = Regexp.union(escape_character, "%", "_")
-
string.gsub(pattern) { |x| [escape_character, x].join }
-
end
-
-
# Accepts an array of conditions. The array has each value
-
# sanitized and interpolated into the SQL statement.
-
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
-
2
def sanitize_sql_array(ary)
-
statement, *values = ary
-
if values.first.is_a?(Hash) && statement =~ /:\w+/
-
replace_named_bind_variables(statement, values.first)
-
elsif statement.include?('?')
-
replace_bind_variables(statement, values)
-
elsif statement.blank?
-
statement
-
else
-
statement % values.collect { |value| connection.quote_string(value.to_s) }
-
end
-
end
-
-
2
def replace_bind_variables(statement, values) #:nodoc:
-
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
-
bound = values.dup
-
c = connection
-
statement.gsub(/\?/) do
-
replace_bind_variable(bound.shift, c)
-
end
-
end
-
-
2
def replace_bind_variable(value, c = connection) #:nodoc:
-
if ActiveRecord::Relation === value
-
value.to_sql
-
else
-
quote_bound_value(value, c)
-
end
-
end
-
-
2
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
-
statement.gsub(/(:?):([a-zA-Z]\w*)/) do
-
if $1 == ':' # skip postgresql casts
-
$& # return the whole match
-
elsif bind_vars.include?(match = $2.to_sym)
-
replace_bind_variable(bind_vars[match])
-
else
-
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
-
end
-
end
-
end
-
-
2
def quote_bound_value(value, c = connection, column = nil) #:nodoc:
-
if column
-
c.quote(value, column)
-
elsif value.respond_to?(:map) && !value.acts_like?(:string)
-
if value.respond_to?(:empty?) && value.empty?
-
c.quote(nil)
-
else
-
value.map { |v| c.quote(v) }.join(',')
-
end
-
else
-
c.quote(value)
-
end
-
end
-
-
2
def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
-
unless expected == provided
-
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
-
end
-
end
-
end
-
-
# TODO: Deprecate this
-
2
def quoted_id
-
self.class.quote_value(id, column_for_attribute(self.class.primary_key))
-
end
-
end
-
end
-
2
require 'active_record/scoping/default'
-
2
require 'active_record/scoping/named'
-
2
require 'active_record/base'
-
-
2
module ActiveRecord
-
2
class SchemaMigration < ActiveRecord::Base
-
2
class << self
-
2
def primary_key
-
nil
-
end
-
-
2
def table_name
-
8
"#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
-
end
-
-
2
def index_name
-
"#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
-
end
-
-
2
def table_exists?
-
connection.table_exists?(table_name)
-
end
-
-
2
def create_table(limit=nil)
-
unless table_exists?
-
version_options = {null: false}
-
version_options[:limit] = limit if limit
-
-
connection.create_table(table_name, id: false) do |t|
-
t.column :version, :string, version_options
-
end
-
connection.add_index table_name, :version, unique: true, name: index_name
-
end
-
end
-
-
2
def drop_table
-
if table_exists?
-
connection.remove_index table_name, name: index_name
-
connection.drop_table(table_name)
-
end
-
end
-
-
2
def normalize_migration_number(number)
-
"%.3d" % number.to_i
-
end
-
-
2
def normalized_versions
-
pluck(:version).map { |v| normalize_migration_number v }
-
end
-
end
-
-
2
def version
-
48
super.to_i
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Scoping
-
2
module Default
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
# Stores the default scope for the class.
-
2
class_attribute :default_scopes, instance_writer: false, instance_predicate: false
-
-
2
self.default_scopes = []
-
end
-
-
2
module ClassMethods
-
# Returns a scope for the model without the previously set scopes.
-
#
-
# class Post < ActiveRecord::Base
-
# def self.default_scope
-
# where published: true
-
# end
-
# end
-
#
-
# Post.all # Fires "SELECT * FROM posts WHERE published = true"
-
# Post.unscoped.all # Fires "SELECT * FROM posts"
-
# Post.where(published: false).unscoped.all # Fires "SELECT * FROM posts"
-
#
-
# This method also accepts a block. All queries inside the block will
-
# not use the previously set scopes.
-
#
-
# Post.unscoped {
-
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
-
# }
-
2
def unscoped
-
160
block_given? ? relation.scoping { yield } : relation
-
end
-
-
2
def before_remove_const #:nodoc:
-
self.current_scope = nil
-
end
-
-
2
protected
-
-
# Use this macro in your model to set a default scope for all operations on
-
# the model.
-
#
-
# class Article < ActiveRecord::Base
-
# default_scope { where(published: true) }
-
# end
-
#
-
# Article.all # => SELECT * FROM articles WHERE published = true
-
#
-
# The +default_scope+ is also applied while creating/building a record.
-
# It is not applied while updating a record.
-
#
-
# Article.new.published # => true
-
# Article.create.published # => true
-
#
-
# (You can also pass any object which responds to +call+ to the
-
# +default_scope+ macro, and it will be called when building the
-
# default scope.)
-
#
-
# If you use multiple +default_scope+ declarations in your model then
-
# they will be merged together:
-
#
-
# class Article < ActiveRecord::Base
-
# default_scope { where(published: true) }
-
# default_scope { where(rating: 'G') }
-
# end
-
#
-
# Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
-
#
-
# This is also the case with inheritance and module includes where the
-
# parent or module defines a +default_scope+ and the child or including
-
# class defines a second one.
-
#
-
# If you need to do more complex things with a default scope, you can
-
# alternatively define it as a class method:
-
#
-
# class Article < ActiveRecord::Base
-
# def self.default_scope
-
# # Should return a scope, you can call 'super' here etc.
-
# end
-
# end
-
2
def default_scope(scope = nil)
-
scope = Proc.new if block_given?
-
-
if scope.is_a?(Relation) || !scope.respond_to?(:call)
-
raise ArgumentError,
-
"Support for calling #default_scope without a block is removed. For example instead " \
-
"of `default_scope where(color: 'red')`, please use " \
-
"`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
-
"self.default_scope.)"
-
end
-
-
self.default_scopes += [scope]
-
end
-
-
2
def build_default_scope(base_rel = relation) # :nodoc:
-
70
return if abstract_class?
-
70
if !Base.is_a?(method(:default_scope).owner)
-
# The user has defined their own default scope method, so call that
-
evaluate_default_scope { default_scope }
-
70
elsif default_scopes.any?
-
evaluate_default_scope do
-
default_scopes.inject(base_rel) do |default_scope, scope|
-
default_scope.merge(base_rel.scoping { scope.call })
-
end
-
end
-
end
-
end
-
-
2
def ignore_default_scope? # :nodoc:
-
ScopeRegistry.value_for(:ignore_default_scope, self)
-
end
-
-
2
def ignore_default_scope=(ignore) # :nodoc:
-
ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore)
-
end
-
-
# The ignore_default_scope flag is used to prevent an infinite recursion
-
# situation where a default scope references a scope which has a default
-
# scope which references a scope...
-
2
def evaluate_default_scope # :nodoc:
-
return if ignore_default_scope?
-
-
begin
-
self.ignore_default_scope = true
-
yield
-
ensure
-
self.ignore_default_scope = false
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/array'
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/kernel/singleton_class'
-
-
2
module ActiveRecord
-
# = Active Record \Named \Scopes
-
2
module Scoping
-
2
module Named
-
2
extend ActiveSupport::Concern
-
-
2
module ClassMethods
-
# Returns an <tt>ActiveRecord::Relation</tt> scope object.
-
#
-
# posts = Post.all
-
# posts.size # Fires "select count(*) from posts" and returns the count
-
# posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
-
#
-
# fruits = Fruit.all
-
# fruits = fruits.where(color: 'red') if options[:red_only]
-
# fruits = fruits.limit(10) if limited?
-
#
-
# You can define a scope that applies to all finders using
-
# <tt>ActiveRecord::Base.default_scope</tt>.
-
2
def all
-
132
if current_scope
-
62
current_scope.clone
-
else
-
70
default_scoped
-
end
-
end
-
-
2
def default_scoped # :nodoc:
-
70
relation.merge(build_default_scope)
-
end
-
-
# Collects attributes from scopes that should be applied when creating
-
# an AR instance for the particular class this is called on.
-
2
def scope_attributes # :nodoc:
-
all.scope_for_create
-
end
-
-
# Are there default attributes associated with this scope?
-
2
def scope_attributes? # :nodoc:
-
21
current_scope || default_scopes.any?
-
end
-
-
# Adds a class method for retrieving and querying objects. A \scope
-
# represents a narrowing of a database query, such as
-
# <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
-
#
-
# class Shirt < ActiveRecord::Base
-
# scope :red, -> { where(color: 'red') }
-
# scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
-
# end
-
#
-
# The above calls to +scope+ define class methods <tt>Shirt.red</tt> and
-
# <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect,
-
# represents the query <tt>Shirt.where(color: 'red')</tt>.
-
#
-
# You should always pass a callable object to the scopes defined
-
# with +scope+. This ensures that the scope is re-evaluated each
-
# time it is called.
-
#
-
# Note that this is simply 'syntactic sugar' for defining an actual
-
# class method:
-
#
-
# class Shirt < ActiveRecord::Base
-
# def self.red
-
# where(color: 'red')
-
# end
-
# end
-
#
-
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
-
# <tt>Shirt.red</tt> is not an Array; it resembles the association object
-
# constructed by a +has_many+ declaration. For instance, you can invoke
-
# <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
-
# <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
-
# association objects, named \scopes act like an Array, implementing
-
# Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
-
# and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
-
# <tt>Shirt.red</tt> really was an Array.
-
#
-
# These named \scopes are composable. For instance,
-
# <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
-
# both red and dry clean only. Nested finds and calculations also work
-
# with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
-
# returns the number of garments for which these criteria obtain.
-
# Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
-
#
-
# All scopes are available as class methods on the ActiveRecord::Base
-
# descendant upon which the \scopes were defined. But they are also
-
# available to +has_many+ associations. If,
-
#
-
# class Person < ActiveRecord::Base
-
# has_many :shirts
-
# end
-
#
-
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
-
# Elton's red, dry clean only shirts.
-
#
-
# \Named scopes can also have extensions, just as with +has_many+
-
# declarations:
-
#
-
# class Shirt < ActiveRecord::Base
-
# scope :red, -> { where(color: 'red') } do
-
# def dom_id
-
# 'red_shirts'
-
# end
-
# end
-
# end
-
#
-
# Scopes can also be used while creating/building a record.
-
#
-
# class Article < ActiveRecord::Base
-
# scope :published, -> { where(published: true) }
-
# end
-
#
-
# Article.published.new.published # => true
-
# Article.published.create.published # => true
-
#
-
# \Class methods on your model are automatically available
-
# on scopes. Assuming the following setup:
-
#
-
# class Article < ActiveRecord::Base
-
# scope :published, -> { where(published: true) }
-
# scope :featured, -> { where(featured: true) }
-
#
-
# def self.latest_article
-
# order('published_at desc').first
-
# end
-
#
-
# def self.titles
-
# pluck(:title)
-
# end
-
# end
-
#
-
# We are able to call the methods like this:
-
#
-
# Article.published.featured.latest_article
-
# Article.featured.titles
-
2
def scope(name, body, &block)
-
2
unless body.respond_to?(:call)
-
raise ArgumentError, 'The scope body needs to be callable.'
-
end
-
-
2
if dangerous_class_method?(name)
-
raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
-
"on the model \"#{self.name}\", but Active Record already defined " \
-
"a class method with the same name."
-
end
-
-
2
extension = Module.new(&block) if block
-
-
2
singleton_class.send(:define_method, name) do |*args|
-
scope = all.scoping { body.call(*args) }
-
scope = scope.extending(extension) if extension
-
-
scope || all
-
end
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord #:nodoc:
-
# = Active Record Serialization
-
2
module Serialization
-
2
extend ActiveSupport::Concern
-
2
include ActiveModel::Serializers::JSON
-
-
2
included do
-
2
self.include_root_in_json = false
-
end
-
-
2
def serializable_hash(options = nil)
-
options = options.try(:clone) || {}
-
-
options[:except] = Array(options[:except]).map { |n| n.to_s }
-
options[:except] |= Array(self.class.inheritance_column)
-
-
super(options)
-
end
-
end
-
end
-
-
2
require 'active_record/serializers/xml_serializer'
-
2
require 'active_support/core_ext/hash/conversions'
-
-
2
module ActiveRecord #:nodoc:
-
2
module Serialization
-
2
include ActiveModel::Serializers::Xml
-
-
# Builds an XML document to represent the model. Some configuration is
-
# available through +options+. However more complicated cases should
-
# override ActiveRecord::Base#to_xml.
-
#
-
# By default the generated XML document will include the processing
-
# instruction and all the object's attributes. For example:
-
#
-
# <?xml version="1.0" encoding="UTF-8"?>
-
# <topic>
-
# <title>The First Topic</title>
-
# <author-name>David</author-name>
-
# <id type="integer">1</id>
-
# <approved type="boolean">false</approved>
-
# <replies-count type="integer">0</replies-count>
-
# <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time>
-
# <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on>
-
# <content>Have a nice day</content>
-
# <author-email-address>david@loudthinking.com</author-email-address>
-
# <parent-id></parent-id>
-
# <last-read type="date">2004-04-15</last-read>
-
# </topic>
-
#
-
# This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
-
# <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
-
# The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
-
# +attributes+ method. The default is to dasherize all column names, but you
-
# can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
-
# to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
-
# To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
-
#
-
# For instance:
-
#
-
# topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
-
#
-
# <topic>
-
# <title>The First Topic</title>
-
# <author-name>David</author-name>
-
# <approved type="boolean">false</approved>
-
# <content>Have a nice day</content>
-
# <author-email-address>david@loudthinking.com</author-email-address>
-
# <parent-id></parent-id>
-
# <last-read type="date">2004-04-15</last-read>
-
# </topic>
-
#
-
# To include first level associations use <tt>:include</tt>:
-
#
-
# firm.to_xml include: [ :account, :clients ]
-
#
-
# <?xml version="1.0" encoding="UTF-8"?>
-
# <firm>
-
# <id type="integer">1</id>
-
# <rating type="integer">1</rating>
-
# <name>37signals</name>
-
# <clients type="array">
-
# <client>
-
# <rating type="integer">1</rating>
-
# <name>Summit</name>
-
# </client>
-
# <client>
-
# <rating type="integer">1</rating>
-
# <name>Microsoft</name>
-
# </client>
-
# </clients>
-
# <account>
-
# <id type="integer">1</id>
-
# <credit-limit type="integer">50</credit-limit>
-
# </account>
-
# </firm>
-
#
-
# Additionally, the record being serialized will be passed to a Proc's second
-
# parameter. This allows for ad hoc additions to the resultant document that
-
# incorporate the context of the record being serialized. And by leveraging the
-
# closure created by a Proc, to_xml can be used to add elements that normally fall
-
# outside of the scope of the model -- for example, generating and appending URLs
-
# associated with models.
-
#
-
# proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
-
# firm.to_xml procs: [ proc ]
-
#
-
# <firm>
-
# # ... normal attributes as shown above ...
-
# <name-reverse>slangis73</name-reverse>
-
# </firm>
-
#
-
# To include deeper levels of associations pass a hash like this:
-
#
-
# firm.to_xml include: {account: {}, clients: {include: :address}}
-
# <?xml version="1.0" encoding="UTF-8"?>
-
# <firm>
-
# <id type="integer">1</id>
-
# <rating type="integer">1</rating>
-
# <name>37signals</name>
-
# <clients type="array">
-
# <client>
-
# <rating type="integer">1</rating>
-
# <name>Summit</name>
-
# <address>
-
# ...
-
# </address>
-
# </client>
-
# <client>
-
# <rating type="integer">1</rating>
-
# <name>Microsoft</name>
-
# <address>
-
# ...
-
# </address>
-
# </client>
-
# </clients>
-
# <account>
-
# <id type="integer">1</id>
-
# <credit-limit type="integer">50</credit-limit>
-
# </account>
-
# </firm>
-
#
-
# To include any methods on the model being called use <tt>:methods</tt>:
-
#
-
# firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
-
#
-
# <firm>
-
# # ... normal attributes as shown above ...
-
# <calculated-earnings>100000000000000000</calculated-earnings>
-
# <real-earnings>5</real-earnings>
-
# </firm>
-
#
-
# To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
-
# modified version of the options hash that was given to +to_xml+:
-
#
-
# proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
-
# firm.to_xml procs: [ proc ]
-
#
-
# <firm>
-
# # ... normal attributes as shown above ...
-
# <abc>def</abc>
-
# </firm>
-
#
-
# Alternatively, you can yield the builder object as part of the +to_xml+ call:
-
#
-
# firm.to_xml do |xml|
-
# xml.creator do
-
# xml.first_name "David"
-
# xml.last_name "Heinemeier Hansson"
-
# end
-
# end
-
#
-
# <firm>
-
# # ... normal attributes as shown above ...
-
# <creator>
-
# <first_name>David</first_name>
-
# <last_name>Heinemeier Hansson</last_name>
-
# </creator>
-
# </firm>
-
#
-
# As noted above, you may override +to_xml+ in your ActiveRecord::Base
-
# subclasses to have complete control about what's generated. The general
-
# form of doing this is:
-
#
-
# class IHaveMyOwnXML < ActiveRecord::Base
-
# def to_xml(options = {})
-
# require 'builder'
-
# options[:indent] ||= 2
-
# xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
-
# xml.instruct! unless options[:skip_instruct]
-
# xml.level_one do
-
# xml.tag!(:second_level, 'content')
-
# end
-
# end
-
# end
-
2
def to_xml(options = {}, &block)
-
XmlSerializer.new(self, options).serialize(&block)
-
end
-
end
-
-
2
class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
-
2
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
-
2
def compute_type
-
klass = @serializable.class
-
column = klass.columns_hash[name] || Type::Value.new
-
-
type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
-
-
{ :text => :string,
-
:time => :datetime }[type] || type
-
end
-
2
protected :compute_type
-
end
-
end
-
end
-
1
module ActiveRecord
-
-
# Statement cache is used to cache a single statement in order to avoid creating the AST again.
-
# Initializing the cache is done by passing the statement in the create block:
-
#
-
# cache = StatementCache.create(Book.connection) do |params|
-
# Book.where(name: "my book").where("author_id > 3")
-
# end
-
#
-
# The cached statement is executed by using the +execute+ method:
-
#
-
# cache.execute([], Book, Book.connection)
-
#
-
# The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
-
# Database is queried when +to_a+ is called on the relation.
-
#
-
# If you want to cache the statement without the values you can use the +bind+ method of the
-
# block parameter.
-
#
-
# cache = StatementCache.create(Book.connection) do |params|
-
# Book.where(name: params.bind)
-
# end
-
#
-
# And pass the bind values as the first argument of +execute+ call.
-
#
-
# cache.execute(["my book"], Book, Book.connection)
-
1
class StatementCache # :nodoc:
-
1
class Substitute; end # :nodoc:
-
-
1
class Query # :nodoc:
-
1
def initialize(sql)
-
6
@sql = sql
-
end
-
-
1
def sql_for(binds, connection)
-
22
@sql
-
end
-
end
-
-
1
class PartialQuery < Query # :nodoc:
-
1
def initialize values
-
@values = values
-
@indexes = values.each_with_index.find_all { |thing,i|
-
Arel::Nodes::BindParam === thing
-
}.map(&:last)
-
end
-
-
1
def sql_for(binds, connection)
-
val = @values.dup
-
binds = binds.dup
-
@indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) }
-
val.join
-
end
-
end
-
-
1
def self.query(visitor, ast)
-
6
Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value
-
end
-
-
1
def self.partial_query(visitor, ast, collector)
-
collected = visitor.accept(ast, collector).value
-
PartialQuery.new collected
-
end
-
-
1
class Params # :nodoc:
-
7
def bind; Substitute.new; end
-
end
-
-
1
class BindMap # :nodoc:
-
1
def initialize(bind_values)
-
6
@indexes = []
-
6
@bind_values = bind_values
-
-
6
bind_values.each_with_index do |(_, value), i|
-
6
if Substitute === value
-
6
@indexes << i
-
end
-
end
-
end
-
-
1
def bind(values)
-
44
bvs = @bind_values.map { |pair| pair.dup }
-
44
@indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] }
-
22
bvs
-
end
-
end
-
-
1
attr_reader :bind_map, :query_builder
-
-
1
def self.create(connection, block = Proc.new)
-
6
relation = block.call Params.new
-
6
bind_map = BindMap.new relation.bind_values
-
6
query_builder = connection.cacheable_query relation.arel
-
6
new query_builder, bind_map
-
end
-
-
1
def initialize(query_builder, bind_map)
-
6
@query_builder = query_builder
-
6
@bind_map = bind_map
-
end
-
-
1
def execute(params, klass, connection)
-
22
bind_values = bind_map.bind params
-
-
22
sql = query_builder.sql_for bind_values, connection
-
-
22
klass.find_by_sql sql, bind_values
-
end
-
1
alias :call :execute
-
end
-
end
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
-
2
module ActiveRecord
-
# Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
-
# It's like a simple key/value store baked into your record when you don't care about being able to
-
# query that store outside the context of a single record.
-
#
-
# You can then declare accessors to this store that are then accessible just like any other attribute
-
# of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
-
# already built around just accessing attributes on the model.
-
#
-
# Make sure that you declare the database column used for the serialized store as a text, so there's
-
# plenty of room.
-
#
-
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
-
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
-
#
-
# NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
-
# the serialization provided by +store+. Simply use +store_accessor+ instead to generate
-
# the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
-
# using a symbol.
-
#
-
# Examples:
-
#
-
# class User < ActiveRecord::Base
-
# store :settings, accessors: [ :color, :homepage ], coder: JSON
-
# end
-
#
-
# u = User.new(color: 'black', homepage: '37signals.com')
-
# u.color # Accessor stored attribute
-
# u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
-
#
-
# # There is no difference between strings and symbols for accessing custom attributes
-
# u.settings[:country] # => 'Denmark'
-
# u.settings['country'] # => 'Denmark'
-
#
-
# # Add additional accessors to an existing store through store_accessor
-
# class SuperUser < User
-
# store_accessor :settings, :privileges, :servants
-
# end
-
#
-
# The stored attribute names can be retrieved using +stored_attributes+.
-
#
-
# User.stored_attributes[:settings] # [:color, :homepage]
-
#
-
# == Overwriting default accessors
-
#
-
# All stored values are automatically available through accessors on the Active Record
-
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
-
# the default accessors (using the same name as the attribute) and calling <tt>super</tt>
-
# to actually change things.
-
#
-
# class Song < ActiveRecord::Base
-
# # Uses a stored integer to hold the volume adjustment of the song
-
# store :settings, accessors: [:volume_adjustment]
-
#
-
# def volume_adjustment=(decibels)
-
# super(decibels.to_i)
-
# end
-
#
-
# def volume_adjustment
-
# super.to_i
-
# end
-
# end
-
2
module Store
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class << self
-
2
attr_accessor :local_stored_attributes
-
end
-
end
-
-
2
module ClassMethods
-
2
def store(store_attribute, options = {})
-
serialize store_attribute, IndifferentCoder.new(options[:coder])
-
store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
-
end
-
-
2
def store_accessor(store_attribute, *keys)
-
keys = keys.flatten
-
-
_store_accessors_module.module_eval do
-
keys.each do |key|
-
define_method("#{key}=") do |value|
-
write_store_attribute(store_attribute, key, value)
-
end
-
-
define_method(key) do
-
read_store_attribute(store_attribute, key)
-
end
-
end
-
end
-
-
# assign new store attribute and create new hash to ensure that each class in the hierarchy
-
# has its own hash of stored attributes.
-
self.local_stored_attributes ||= {}
-
self.local_stored_attributes[store_attribute] ||= []
-
self.local_stored_attributes[store_attribute] |= keys
-
end
-
-
2
def _store_accessors_module # :nodoc:
-
@_store_accessors_module ||= begin
-
mod = Module.new
-
include mod
-
mod
-
end
-
end
-
-
2
def stored_attributes
-
parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
-
if self.local_stored_attributes
-
parent.merge!(self.local_stored_attributes) { |k, a, b| a | b }
-
end
-
parent
-
end
-
end
-
-
2
protected
-
2
def read_store_attribute(store_attribute, key)
-
accessor = store_accessor_for(store_attribute)
-
accessor.read(self, store_attribute, key)
-
end
-
-
2
def write_store_attribute(store_attribute, key, value)
-
accessor = store_accessor_for(store_attribute)
-
accessor.write(self, store_attribute, key, value)
-
end
-
-
2
private
-
2
def store_accessor_for(store_attribute)
-
type_for_attribute(store_attribute.to_s).accessor
-
end
-
-
2
class HashAccessor # :nodoc:
-
2
def self.read(object, attribute, key)
-
prepare(object, attribute)
-
object.public_send(attribute)[key]
-
end
-
-
2
def self.write(object, attribute, key, value)
-
prepare(object, attribute)
-
if value != read(object, attribute, key)
-
object.public_send :"#{attribute}_will_change!"
-
object.public_send(attribute)[key] = value
-
end
-
end
-
-
2
def self.prepare(object, attribute)
-
object.public_send :"#{attribute}=", {} unless object.send(attribute)
-
end
-
end
-
-
2
class StringKeyedHashAccessor < HashAccessor # :nodoc:
-
2
def self.read(object, attribute, key)
-
super object, attribute, key.to_s
-
end
-
-
2
def self.write(object, attribute, key, value)
-
super object, attribute, key.to_s, value
-
end
-
end
-
-
2
class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
-
2
def self.prepare(object, store_attribute)
-
attribute = object.send(store_attribute)
-
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
-
attribute = IndifferentCoder.as_indifferent_hash(attribute)
-
object.send :"#{store_attribute}=", attribute
-
end
-
attribute
-
end
-
end
-
-
2
class IndifferentCoder # :nodoc:
-
2
def initialize(coder_or_class_name)
-
@coder =
-
if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
-
coder_or_class_name
-
else
-
ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object)
-
end
-
end
-
-
2
def dump(obj)
-
@coder.dump self.class.as_indifferent_hash(obj)
-
end
-
-
2
def load(yaml)
-
self.class.as_indifferent_hash(@coder.load(yaml || ''))
-
end
-
-
2
def self.as_indifferent_hash(obj)
-
case obj
-
when ActiveSupport::HashWithIndifferentAccess
-
obj
-
when Hash
-
obj.with_indifferent_access
-
else
-
ActiveSupport::HashWithIndifferentAccess.new
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record Timestamp
-
#
-
# Active Record automatically timestamps create and update operations if the
-
# table has fields named <tt>created_at/created_on</tt> or
-
# <tt>updated_at/updated_on</tt>.
-
#
-
# Timestamping can be turned off by setting:
-
#
-
# config.active_record.record_timestamps = false
-
#
-
# Timestamps are in UTC by default but you can use the local timezone by setting:
-
#
-
# config.active_record.default_timezone = :local
-
#
-
# == Time Zone aware attributes
-
#
-
# By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code.
-
#
-
# config.active_record.time_zone_aware_attributes = true
-
#
-
# This feature can easily be turned off by assigning value <tt>false</tt> .
-
#
-
# If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone
-
# when reading certain attributes then you can do following:
-
#
-
# class Topic < ActiveRecord::Base
-
# self.skip_time_zone_conversion_for_attributes = [:written_on]
-
# end
-
2
module Timestamp
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :record_timestamps
-
2
self.record_timestamps = true
-
end
-
-
2
def initialize_dup(other) # :nodoc:
-
super
-
clear_timestamp_attributes
-
end
-
-
2
private
-
-
2
def _create_record
-
6
if self.record_timestamps
-
6
current_time = current_time_from_proper_timezone
-
-
6
all_timestamp_attributes.each do |column|
-
24
column = column.to_s
-
24
if has_attribute?(column) && !attribute_present?(column)
-
12
write_attribute(column, current_time)
-
end
-
end
-
end
-
-
6
super
-
end
-
-
2
def _update_record(*args)
-
4
if should_record_timestamps?
-
2
current_time = current_time_from_proper_timezone
-
-
2
timestamp_attributes_for_update_in_model.each do |column|
-
2
column = column.to_s
-
2
next if attribute_changed?(column)
-
2
write_attribute(column, current_time)
-
end
-
end
-
4
super
-
end
-
-
2
def should_record_timestamps?
-
4
self.record_timestamps && (!partial_writes? || changed?)
-
end
-
-
2
def timestamp_attributes_for_create_in_model
-
timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) }
-
end
-
-
2
def timestamp_attributes_for_update_in_model
-
6
timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) }
-
end
-
-
2
def all_timestamp_attributes_in_model
-
timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
-
end
-
-
2
def timestamp_attributes_for_update
-
8
[:updated_at, :updated_on]
-
end
-
-
2
def timestamp_attributes_for_create
-
6
[:created_at, :created_on]
-
end
-
-
2
def all_timestamp_attributes
-
6
timestamp_attributes_for_create + timestamp_attributes_for_update
-
end
-
-
2
def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
-
timestamp_names
-
.map { |attr| self[attr] }
-
.compact
-
.map(&:to_time)
-
.max
-
end
-
-
2
def current_time_from_proper_timezone
-
8
self.class.default_timezone == :utc ? Time.now.utc : Time.now
-
end
-
-
# Clear attributes and changed_attributes
-
2
def clear_timestamp_attributes
-
all_timestamp_attributes_in_model.each do |attribute_name|
-
self[attribute_name] = nil
-
clear_attribute_changes([attribute_name])
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
# See ActiveRecord::Transactions::ClassMethods for documentation.
-
2
module Transactions
-
2
extend ActiveSupport::Concern
-
#:nodoc:
-
2
ACTIONS = [:create, :destroy, :update]
-
#:nodoc:
-
2
CALLBACK_WARN_MESSAGE = "Currently, Active Record suppresses errors raised " \
-
"within `after_rollback`/`after_commit` callbacks and only print them to " \
-
"the logs. In the next version, these errors will no longer be suppressed. " \
-
"Instead, the errors will propagate normally just like in other Active " \
-
"Record callbacks.\n" \
-
"\n" \
-
"You can opt into the new behavior and remove this warning by setting:\n" \
-
"\n" \
-
" config.active_record.raise_in_transactional_callbacks = true\n\n"
-
-
2
included do
-
2
define_callbacks :commit, :rollback,
-
terminator: ->(_, result) { result == false },
-
scope: [:kind, :name]
-
-
2
mattr_accessor :raise_in_transactional_callbacks, instance_writer: false
-
2
self.raise_in_transactional_callbacks = false
-
end
-
-
# = Active Record Transactions
-
#
-
# Transactions are protective blocks where SQL statements are only permanent
-
# if they can all succeed as one atomic action. The classic example is a
-
# transfer between two accounts where you can only have a deposit if the
-
# withdrawal succeeded and vice versa. Transactions enforce the integrity of
-
# the database and guard the data against program errors or database
-
# break-downs. So basically you should use transaction blocks whenever you
-
# have a number of statements that must be executed together or not at all.
-
#
-
# For example:
-
#
-
# ActiveRecord::Base.transaction do
-
# david.withdrawal(100)
-
# mary.deposit(100)
-
# end
-
#
-
# This example will only take money from David and give it to Mary if neither
-
# +withdrawal+ nor +deposit+ raise an exception. Exceptions will force a
-
# ROLLBACK that returns the database to the state before the transaction
-
# began. Be aware, though, that the objects will _not_ have their instance
-
# data returned to their pre-transactional state.
-
#
-
# == Different Active Record classes in a single transaction
-
#
-
# Though the transaction class method is called on some Active Record class,
-
# the objects within the transaction block need not all be instances of
-
# that class. This is because transactions are per-database connection, not
-
# per-model.
-
#
-
# In this example a +balance+ record is transactionally saved even
-
# though +transaction+ is called on the +Account+ class:
-
#
-
# Account.transaction do
-
# balance.save!
-
# account.save!
-
# end
-
#
-
# The +transaction+ method is also available as a model instance method.
-
# For example, you can also do this:
-
#
-
# balance.transaction do
-
# balance.save!
-
# account.save!
-
# end
-
#
-
# == Transactions are not distributed across database connections
-
#
-
# A transaction acts on a single database connection. If you have
-
# multiple class-specific databases, the transaction will not protect
-
# interaction among them. One workaround is to begin a transaction
-
# on each class whose models you alter:
-
#
-
# Student.transaction do
-
# Course.transaction do
-
# course.enroll(student)
-
# student.units += course.units
-
# end
-
# end
-
#
-
# This is a poor solution, but fully distributed transactions are beyond
-
# the scope of Active Record.
-
#
-
# == +save+ and +destroy+ are automatically wrapped in a transaction
-
#
-
# Both +save+ and +destroy+ come wrapped in a transaction that ensures
-
# that whatever you do in validations or callbacks will happen under its
-
# protected cover. So you can use validations to check for values that
-
# the transaction depends on or you can raise exceptions in the callbacks
-
# to rollback, including <tt>after_*</tt> callbacks.
-
#
-
# As a consequence changes to the database are not seen outside your connection
-
# until the operation is complete. For example, if you try to update the index
-
# of a search engine in +after_save+ the indexer won't see the updated record.
-
# The +after_commit+ callback is the only one that is triggered once the update
-
# is committed. See below.
-
#
-
# == Exception handling and rolling back
-
#
-
# Also have in mind that exceptions thrown within a transaction block will
-
# be propagated (after triggering the ROLLBACK), so you should be ready to
-
# catch those in your application code.
-
#
-
# One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger
-
# a ROLLBACK when raised, but not be re-raised by the transaction block.
-
#
-
# *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions
-
# inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an
-
# error occurred at the database level, for example when a unique constraint
-
# is violated. On some database systems, such as PostgreSQL, database errors
-
# inside a transaction cause the entire transaction to become unusable
-
# until it's restarted from the beginning. Here is an example which
-
# demonstrates the problem:
-
#
-
# # Suppose that we have a Number model with a unique column called 'i'.
-
# Number.transaction do
-
# Number.create(i: 0)
-
# begin
-
# # This will raise a unique constraint error...
-
# Number.create(i: 0)
-
# rescue ActiveRecord::StatementInvalid
-
# # ...which we ignore.
-
# end
-
#
-
# # On PostgreSQL, the transaction is now unusable. The following
-
# # statement will cause a PostgreSQL error, even though the unique
-
# # constraint is no longer violated:
-
# Number.create(i: 1)
-
# # => "PGError: ERROR: current transaction is aborted, commands
-
# # ignored until end of transaction block"
-
# end
-
#
-
# One should restart the entire transaction if an
-
# <tt>ActiveRecord::StatementInvalid</tt> occurred.
-
#
-
# == Nested transactions
-
#
-
# +transaction+ calls can be nested. By default, this makes all database
-
# statements in the nested transaction block become part of the parent
-
# transaction. For example, the following behavior may be surprising:
-
#
-
# User.transaction do
-
# User.create(username: 'Kotori')
-
# User.transaction do
-
# User.create(username: 'Nemu')
-
# raise ActiveRecord::Rollback
-
# end
-
# end
-
#
-
# creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
-
# exception in the nested block does not issue a ROLLBACK. Since these exceptions
-
# are captured in transaction blocks, the parent block does not see it and the
-
# real transaction is committed.
-
#
-
# In order to get a ROLLBACK for the nested transaction you may ask for a real
-
# sub-transaction by passing <tt>requires_new: true</tt>. If anything goes wrong,
-
# the database rolls back to the beginning of the sub-transaction without rolling
-
# back the parent transaction. If we add it to the previous example:
-
#
-
# User.transaction do
-
# User.create(username: 'Kotori')
-
# User.transaction(requires_new: true) do
-
# User.create(username: 'Nemu')
-
# raise ActiveRecord::Rollback
-
# end
-
# end
-
#
-
# only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
-
#
-
# Most databases don't support true nested transactions. At the time of
-
# writing, the only database that we're aware of that supports true nested
-
# transactions, is MS-SQL. Because of this, Active Record emulates nested
-
# transactions by using savepoints on MySQL and PostgreSQL. See
-
# http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
-
# for more information about savepoints.
-
#
-
# === Callbacks
-
#
-
# There are two types of callbacks associated with committing and rolling back transactions:
-
# +after_commit+ and +after_rollback+.
-
#
-
# +after_commit+ callbacks are called on every record saved or destroyed within a
-
# transaction immediately after the transaction is committed. +after_rollback+ callbacks
-
# are called on every record saved or destroyed within a transaction immediately after the
-
# transaction or savepoint is rolled back.
-
#
-
# These callbacks are useful for interacting with other systems since you will be guaranteed
-
# that the callback is only executed when the database is in a permanent state. For example,
-
# +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
-
# within a transaction could trigger the cache to be regenerated before the database is updated.
-
#
-
# === Caveats
-
#
-
# If you're on MySQL, then do not use DDL operations in nested transactions
-
# blocks that are emulated with savepoints. That is, do not execute statements
-
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
-
# releases all savepoints upon executing a DDL operation. When +transaction+
-
# is finished and tries to release the savepoint it created earlier, a
-
# database error will occur because the savepoint has already been
-
# automatically released. The following example demonstrates the problem:
-
#
-
# Model.connection.transaction do # BEGIN
-
# Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
-
# Model.connection.create_table(...) # active_record_1 now automatically released
-
# end # RELEASE savepoint active_record_1
-
# # ^^^^ BOOM! database error!
-
# end
-
#
-
# Note that "TRUNCATE" is also a MySQL DDL statement!
-
2
module ClassMethods
-
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
-
2
def transaction(options = {}, &block)
-
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
-
23
connection.transaction(options, &block)
-
end
-
-
# This callback is called after a record has been created, updated, or destroyed.
-
#
-
# You can specify that the callback should only be fired by a certain action with
-
# the +:on+ option:
-
#
-
# after_commit :do_foo, on: :create
-
# after_commit :do_bar, on: :update
-
# after_commit :do_baz, on: :destroy
-
#
-
# after_commit :do_foo_bar, on: [:create, :update]
-
# after_commit :do_bar_baz, on: [:update, :destroy]
-
#
-
# Note that transactional fixtures do not play well with this feature. Please
-
# use the +test_after_commit+ gem to have these hooks fired in tests.
-
2
def after_commit(*args, &block)
-
set_options_for_callbacks!(args)
-
set_callback(:commit, :after, *args, &block)
-
unless ActiveRecord::Base.raise_in_transactional_callbacks
-
ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE)
-
end
-
end
-
-
# This callback is called after a create, update, or destroy are rolled back.
-
#
-
# Please check the documentation of +after_commit+ for options.
-
2
def after_rollback(*args, &block)
-
set_options_for_callbacks!(args)
-
set_callback(:rollback, :after, *args, &block)
-
unless ActiveRecord::Base.raise_in_transactional_callbacks
-
ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE)
-
end
-
end
-
-
2
private
-
-
2
def set_options_for_callbacks!(args)
-
options = args.last
-
if options.is_a?(Hash) && options[:on]
-
fire_on = Array(options[:on])
-
assert_valid_transaction_action(fire_on)
-
options[:if] = Array(options[:if])
-
options[:if] << "transaction_include_any_action?(#{fire_on})"
-
end
-
end
-
-
2
def assert_valid_transaction_action(actions)
-
if (actions - ACTIONS).any?
-
raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}"
-
end
-
end
-
end
-
-
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
-
2
def transaction(options = {}, &block)
-
self.class.transaction(options, &block)
-
end
-
-
2
def destroy #:nodoc:
-
6
with_transaction_returning_status { super }
-
end
-
-
2
def save(*) #:nodoc:
-
14
rollback_active_record_state! do
-
28
with_transaction_returning_status { super }
-
end
-
end
-
-
2
def save!(*) #:nodoc:
-
with_transaction_returning_status { super }
-
end
-
-
2
def touch(*) #:nodoc:
-
with_transaction_returning_status { super }
-
end
-
-
# Reset id and @new_record if the transaction rolls back.
-
2
def rollback_active_record_state!
-
14
remember_transaction_record_state
-
14
yield
-
rescue Exception
-
restore_transaction_record_state
-
raise
-
ensure
-
14
clear_transaction_record_state
-
end
-
-
# Call the +after_commit+ callbacks.
-
#
-
# Ensure that it is not called if the object was never persisted (failed create),
-
# but call it after the commit of a destroyed object.
-
2
def committed!(should_run_callbacks = true) #:nodoc:
-
_run_commit_callbacks if should_run_callbacks && destroyed? || persisted?
-
ensure
-
force_clear_transaction_record_state
-
end
-
-
# Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
-
# state should be rolled back to the beginning or just to the last savepoint.
-
2
def rolledback!(force_restore_state = false, should_run_callbacks = true) #:nodoc:
-
3
_run_rollback_callbacks if should_run_callbacks
-
ensure
-
3
restore_transaction_record_state(force_restore_state)
-
3
clear_transaction_record_state
-
end
-
-
# Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
-
# can be called.
-
2
def add_to_transaction
-
23
if has_transactional_callbacks?
-
self.class.connection.add_transaction_record(self)
-
else
-
23
sync_with_transaction_state
-
23
set_transaction_state(self.class.connection.transaction_state)
-
end
-
23
remember_transaction_record_state
-
end
-
-
# Executes +method+ within a transaction and captures its return value as a
-
# status flag. If the status is true the transaction is committed, otherwise
-
# a ROLLBACK is issued. In any case the status flag is returned.
-
#
-
# This method is available within the context of an ActiveRecord::Base
-
# instance.
-
2
def with_transaction_returning_status
-
23
status = nil
-
23
self.class.transaction do
-
23
add_to_transaction
-
23
begin
-
23
status = yield
-
rescue ActiveRecord::Rollback
-
clear_transaction_record_state
-
status = nil
-
end
-
-
23
raise ActiveRecord::Rollback unless status
-
end
-
23
status
-
ensure
-
23
if @transaction_state && @transaction_state.committed?
-
13
clear_transaction_record_state
-
end
-
end
-
-
2
protected
-
-
# Save the new record state and id of a record so it can be restored later if a transaction fails.
-
2
def remember_transaction_record_state #:nodoc:
-
37
@_start_transaction_state[:id] = id
-
37
@_start_transaction_state.reverse_merge!(
-
new_record: @new_record,
-
destroyed: @destroyed,
-
frozen?: frozen?,
-
)
-
37
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
-
end
-
-
# Clear the new record state and id of a record.
-
2
def clear_transaction_record_state #:nodoc:
-
62
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
-
62
force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
-
end
-
-
# Force to clear the transaction record state.
-
2
def force_clear_transaction_record_state #:nodoc:
-
42
@_start_transaction_state.clear
-
end
-
-
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
-
2
def restore_transaction_record_state(force = false) #:nodoc:
-
23
unless @_start_transaction_state.empty?
-
6
transaction_level = (@_start_transaction_state[:level] || 0) - 1
-
6
if transaction_level < 1 || force
-
4
restore_state = @_start_transaction_state
-
4
thaw
-
4
@new_record = restore_state[:new_record]
-
4
@destroyed = restore_state[:destroyed]
-
4
pk = self.class.primary_key
-
4
if pk && read_attribute(pk) != restore_state[:id]
-
write_attribute(pk, restore_state[:id])
-
end
-
4
freeze if restore_state[:frozen?]
-
end
-
end
-
end
-
-
# Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
-
2
def transaction_record_state(state) #:nodoc:
-
@_start_transaction_state[state]
-
end
-
-
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
-
2
def transaction_include_any_action?(actions) #:nodoc:
-
actions.any? do |action|
-
case action
-
when :create
-
transaction_record_state(:new_record)
-
when :destroy
-
destroyed?
-
when :update
-
!(transaction_record_state(:new_record) || destroyed?)
-
end
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Translation
-
2
include ActiveModel::Translation
-
-
# Set the lookup ancestors for ActiveModel.
-
2
def lookup_ancestors #:nodoc:
-
94
klass = self
-
94
classes = [klass]
-
94
return classes if klass == ActiveRecord::Base
-
-
94
while klass != klass.base_class
-
classes << klass = klass.superclass
-
end
-
94
classes
-
end
-
-
# Set the i18n scope to overwrite ActiveModel.
-
2
def i18n_scope #:nodoc:
-
128
:activerecord
-
end
-
end
-
end
-
2
module ActiveRecord
-
# = Active Record RecordInvalid
-
#
-
# Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
-
# +record+ method to retrieve the record which did not validate.
-
#
-
# begin
-
# complex_operation_that_internally_calls_save!
-
# rescue ActiveRecord::RecordInvalid => invalid
-
# puts invalid.record.errors
-
# end
-
2
class RecordInvalid < ActiveRecordError
-
2
attr_reader :record
-
-
2
def initialize(record)
-
@record = record
-
errors = @record.errors.full_messages.join(", ")
-
super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
-
end
-
end
-
-
# = Active Record Validations
-
#
-
# Active Record includes the majority of its validations from <tt>ActiveModel::Validations</tt>
-
# all of which accept the <tt>:on</tt> argument to define the context where the
-
# validations are active. Active Record will always supply either the context of
-
# <tt>:create</tt> or <tt>:update</tt> dependent on whether the model is a
-
# <tt>new_record?</tt>.
-
2
module Validations
-
2
extend ActiveSupport::Concern
-
2
include ActiveModel::Validations
-
-
# The validation process on save can be skipped by passing <tt>validate: false</tt>.
-
# The regular Base#save method is replaced with this when the validations
-
# module is mixed in, which it is by default.
-
2
def save(options={})
-
14
perform_validations(options) ? super : false
-
end
-
-
# Attempts to save the record just like Base#save but will raise a +RecordInvalid+
-
# exception instead of returning +false+ if the record is not valid.
-
2
def save!(options={})
-
perform_validations(options) ? super : raise_record_invalid
-
end
-
-
# Runs all the validations within the specified context. Returns +true+ if
-
# no errors are found, +false+ otherwise.
-
#
-
# Aliased as validate.
-
#
-
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
-
# <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
-
#
-
# Validations with no <tt>:on</tt> option will run no matter the context. Validations with
-
# some <tt>:on</tt> option will only run in the specified context.
-
2
def valid?(context = nil)
-
21
context ||= (new_record? ? :create : :update)
-
21
output = super(context)
-
21
errors.empty? && output
-
end
-
-
2
alias_method :validate, :valid?
-
-
# Runs all the validations within the specified context. Returns +true+ if
-
# no errors are found, raises +RecordInvalid+ otherwise.
-
#
-
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
-
# <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
-
#
-
# Validations with no <tt>:on</tt> option will run no matter the context. Validations with
-
# some <tt>:on</tt> option will only run in the specified context.
-
2
def validate!(context = nil)
-
valid?(context) || raise_record_invalid
-
end
-
-
2
protected
-
-
2
def raise_record_invalid
-
raise(RecordInvalid.new(self))
-
end
-
-
2
def perform_validations(options={}) # :nodoc:
-
14
options[:validate] == false || valid?(options[:context])
-
end
-
end
-
end
-
-
2
require "active_record/validations/associated"
-
2
require "active_record/validations/uniqueness"
-
2
require "active_record/validations/presence"
-
2
module ActiveRecord
-
2
module Validations
-
2
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
-
2
def validate_each(record, attribute, value)
-
if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
-
record.errors.add(attribute, :invalid, options.merge(:value => value))
-
end
-
end
-
end
-
-
2
module ClassMethods
-
# Validates whether the associated object or objects are all valid.
-
# Works with any kind of association.
-
#
-
# class Book < ActiveRecord::Base
-
# has_many :pages
-
# belongs_to :library
-
#
-
# validates_associated :pages, :library
-
# end
-
#
-
# WARNING: This validation must not be used on both ends of an association.
-
# Doing so will lead to a circular dependency and cause infinite recursion.
-
#
-
# NOTE: This validation will not fail if the association hasn't been
-
# assigned. If you want to ensure that the association is both present and
-
# guaranteed to be valid, you also need to use +validates_presence_of+.
-
#
-
# Configuration options:
-
#
-
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
-
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
-
# Runs in all validation contexts by default (nil). You can pass a symbol
-
# or an array of symbols. (e.g. <tt>on: :create</tt> or
-
# <tt>on: :custom_validation_context</tt> or
-
# <tt>on: [:create, :custom_validation_context]</tt>)
-
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
-
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
-
# proc or string should return or evaluate to a +true+ or +false+ value.
-
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
-
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
-
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
-
# method, proc or string should return or evaluate to a +true+ or +false+
-
# value.
-
2
def validates_associated(*attr_names)
-
validates_with AssociatedValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Validations
-
2
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
-
2
def validate(record)
-
15
super
-
15
attributes.each do |attribute|
-
46
next unless record.class._reflect_on_association(attribute)
-
associated_records = Array.wrap(record.send(attribute))
-
-
# Superclass validates presence. Ensure present records aren't about to be destroyed.
-
if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
-
record.errors.add(attribute, :blank, options)
-
end
-
end
-
end
-
end
-
-
2
module ClassMethods
-
# Validates that the specified attributes are not blank (as defined by
-
# Object#blank?), and, if the attribute is an association, that the
-
# associated object is not marked for destruction. Happens by default
-
# on save.
-
#
-
# class Person < ActiveRecord::Base
-
# has_one :face
-
# validates_presence_of :face
-
# end
-
#
-
# The face attribute must be in the object and it cannot be blank or marked
-
# for destruction.
-
#
-
# If you want to validate the presence of a boolean field (where the real values
-
# are true and false), you will want to use
-
# <tt>validates_inclusion_of :field_name, in: [true, false]</tt>.
-
#
-
# This is due to the way Object#blank? handles boolean values:
-
# <tt>false.blank? # => true</tt>.
-
#
-
# This validator defers to the ActiveModel validation for presence, adding the
-
# check to see that an associated object is not marked for destruction. This
-
# prevents the parent object from validating successfully and saving, which then
-
# deletes the associated object, thus putting the parent object into an invalid
-
# state.
-
#
-
# Configuration options:
-
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
-
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
-
# Runs in all validation contexts by default (nil). You can pass a symbol
-
# or an array of symbols. (e.g. <tt>on: :create</tt> or
-
# <tt>on: :custom_validation_context</tt> or
-
# <tt>on: [:create, :custom_validation_context]</tt>)
-
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
-
# the validation should occur (e.g. <tt>if: :allow_validation</tt>, or
-
# <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
-
# or string should return or evaluate to a +true+ or +false+ value.
-
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
-
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method,
-
# proc or string should return or evaluate to a +true+ or +false+ value.
-
# * <tt>:strict</tt> - Specifies whether validation should be strict.
-
# See <tt>ActiveModel::Validation#validates!</tt> for more information.
-
2
def validates_presence_of(*attr_names)
-
8
validates_with PresenceValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
module ActiveRecord
-
2
module Validations
-
2
class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
-
2
def initialize(options)
-
6
if options[:conditions] && !options[:conditions].respond_to?(:call)
-
raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
-
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
-
end
-
6
super({ case_sensitive: true }.merge!(options))
-
6
@klass = options[:class]
-
end
-
-
2
def validate_each(record, attribute, value)
-
15
finder_class = find_finder_class_for(record)
-
15
table = finder_class.arel_table
-
15
value = map_enum_attribute(finder_class, attribute, value)
-
-
15
begin
-
15
relation = build_relation(finder_class, table, attribute, value)
-
15
if record.persisted?
-
4
if finder_class.primary_key
-
4
relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id))
-
else
-
raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
-
end
-
end
-
15
relation = scope_relation(record, table, relation)
-
15
relation = finder_class.unscoped.where(relation)
-
15
relation = relation.merge(options[:conditions]) if options[:conditions]
-
rescue RangeError
-
relation = finder_class.none
-
end
-
-
15
if relation.exists?
-
6
error_options = options.except(:case_sensitive, :scope, :conditions)
-
6
error_options[:value] = value
-
-
6
record.errors.add(attribute, :taken, error_options)
-
end
-
end
-
-
2
protected
-
# The check for an existing value should be run from a class that
-
# isn't abstract. This means working down from the current class
-
# (self), to the first non-abstract class. Since classes don't know
-
# their subclasses, we have to build the hierarchy between self and
-
# the record's class.
-
2
def find_finder_class_for(record) #:nodoc:
-
15
class_hierarchy = [record.class]
-
-
15
while class_hierarchy.first != @klass
-
class_hierarchy.unshift(class_hierarchy.first.superclass)
-
end
-
-
30
class_hierarchy.detect { |klass| !klass.abstract_class? }
-
end
-
-
2
def build_relation(klass, table, attribute, value) #:nodoc:
-
15
if reflection = klass._reflect_on_association(attribute)
-
attribute = reflection.foreign_key
-
value = value.attributes[reflection.klass.primary_key] unless value.nil?
-
end
-
-
15
attribute_name = attribute.to_s
-
-
# the attribute may be an aliased attribute
-
15
if klass.attribute_aliases[attribute_name]
-
attribute = klass.attribute_aliases[attribute_name]
-
attribute_name = attribute.to_s
-
end
-
-
15
column = klass.columns_hash[attribute_name]
-
15
value = klass.connection.type_cast(value, column)
-
15
if value.is_a?(String) && column.limit
-
value = value.to_s[0, column.limit]
-
end
-
-
15
if !options[:case_sensitive] && value && column.text?
-
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
-
klass.connection.case_insensitive_comparison(table, attribute, column, value)
-
else
-
15
klass.connection.case_sensitive_comparison(table, attribute, column, value)
-
end
-
end
-
-
2
def scope_relation(record, table, relation)
-
15
Array(options[:scope]).each do |scope_item|
-
if reflection = record.class._reflect_on_association(scope_item)
-
scope_value = record.send(reflection.foreign_key)
-
scope_item = reflection.foreign_key
-
else
-
scope_value = record._read_attribute(scope_item)
-
end
-
relation = relation.and(table[scope_item].eq(scope_value))
-
end
-
-
15
relation
-
end
-
-
2
def map_enum_attribute(klass, attribute, value)
-
15
mapping = klass.defined_enums[attribute.to_s]
-
15
value = mapping[value] if value && mapping
-
15
value
-
end
-
end
-
-
2
module ClassMethods
-
# Validates whether the value of the specified attributes are unique
-
# across the system. Useful for making sure that only one user
-
# can be named "davidhh".
-
#
-
# class Person < ActiveRecord::Base
-
# validates_uniqueness_of :user_name
-
# end
-
#
-
# It can also validate whether the value of the specified attributes are
-
# unique based on a <tt>:scope</tt> parameter:
-
#
-
# class Person < ActiveRecord::Base
-
# validates_uniqueness_of :user_name, scope: :account_id
-
# end
-
#
-
# Or even multiple scope parameters. For example, making sure that a
-
# teacher can only be on the schedule once per semester for a particular
-
# class.
-
#
-
# class TeacherSchedule < ActiveRecord::Base
-
# validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
-
# end
-
#
-
# It is also possible to limit the uniqueness constraint to a set of
-
# records matching certain conditions. In this example archived articles
-
# are not being taken into consideration when validating uniqueness
-
# of the title attribute:
-
#
-
# class Article < ActiveRecord::Base
-
# validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
-
# end
-
#
-
# When the record is created, a check is performed to make sure that no
-
# record exists in the database with the given value for the specified
-
# attribute (that maps to a column). When the record is updated,
-
# the same check is made but disregarding the record itself.
-
#
-
# Configuration options:
-
#
-
# * <tt>:message</tt> - Specifies a custom error message (default is:
-
# "has already been taken").
-
# * <tt>:scope</tt> - One or more columns by which to limit the scope of
-
# the uniqueness constraint.
-
# * <tt>:conditions</tt> - Specify the conditions to be included as a
-
# <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
-
# (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
-
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
-
# non-text columns (+true+ by default).
-
# * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
-
# attribute is +nil+ (default is +false+).
-
# * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
-
# attribute is blank (default is +false+).
-
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
-
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
-
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
-
# proc or string should return or evaluate to a +true+ or +false+ value.
-
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
-
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
-
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
-
# method, proc or string should return or evaluate to a +true+ or +false+
-
# value.
-
#
-
# === Concurrency and integrity
-
#
-
# Using this validation method in conjunction with ActiveRecord::Base#save
-
# does not guarantee the absence of duplicate record insertions, because
-
# uniqueness checks on the application level are inherently prone to race
-
# conditions. For example, suppose that two users try to post a Comment at
-
# the same time, and a Comment's title must be unique. At the database-level,
-
# the actions performed by these users could be interleaved in the following manner:
-
#
-
# User 1 | User 2
-
# ------------------------------------+--------------------------------------
-
# # User 1 checks whether there's |
-
# # already a comment with the title |
-
# # 'My Post'. This is not the case. |
-
# SELECT * FROM comments |
-
# WHERE title = 'My Post' |
-
# |
-
# | # User 2 does the same thing and also
-
# | # infers that their title is unique.
-
# | SELECT * FROM comments
-
# | WHERE title = 'My Post'
-
# |
-
# # User 1 inserts their comment. |
-
# INSERT INTO comments |
-
# (title, content) VALUES |
-
# ('My Post', 'hi!') |
-
# |
-
# | # User 2 does the same thing.
-
# | INSERT INTO comments
-
# | (title, content) VALUES
-
# | ('My Post', 'hello!')
-
# |
-
# | # ^^^^^^
-
# | # Boom! We now have a duplicate
-
# | # title!
-
#
-
# This could even happen if you use transactions with the 'serializable'
-
# isolation level. The best way to work around this problem is to add a unique
-
# index to the database table using
-
# ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
-
# rare case that a race condition occurs, the database will guarantee
-
# the field's uniqueness.
-
#
-
# When the database catches such a duplicate insertion,
-
# ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
-
# exception. You can either choose to let this error propagate (which
-
# will result in the default Rails exception page being shown), or you
-
# can catch it and restart the transaction (e.g. by telling the user
-
# that the title already exists, and asking them to re-enter the title).
-
# This technique is also known as
-
# {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
-
#
-
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
-
# constraint errors from other types of database errors by throwing an
-
# ActiveRecord::RecordNotUnique exception. For other adapters you will
-
# have to parse the (database-specific) exception message to detect such
-
# a case.
-
#
-
# The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
-
#
-
# * ActiveRecord::ConnectionAdapters::MysqlAdapter.
-
# * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
-
# * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
-
# * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
-
2
def validates_uniqueness_of(*attr_names)
-
6
validates_with UniquenessValidator, _merge_attributes(attr_names)
-
end
-
end
-
end
-
end
-
2
require 'active_support'
-
2
require 'active_support/time'
-
2
require 'active_support/core_ext'
-
2
module ActiveSupport
-
# Backtraces often include many lines that are not relevant for the context
-
# under review. This makes it hard to find the signal amongst the backtrace
-
# noise, and adds debugging time. With a BacktraceCleaner, filters and
-
# silencers are used to remove the noisy lines, so that only the most relevant
-
# lines remain.
-
#
-
# Filters are used to modify lines of data, while silencers are used to remove
-
# lines entirely. The typical filter use case is to remove lengthy path
-
# information from the start of each line, and view file paths relevant to the
-
# app directory instead of the file system root. The typical silencer use case
-
# is to exclude the output of a noisy library from the backtrace, so that you
-
# can focus on the rest.
-
#
-
# bc = BacktraceCleaner.new
-
# bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
-
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems
-
# bc.clean(exception.backtrace) # perform the cleanup
-
#
-
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
-
# and show as much data as possible, you can always call
-
# <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the
-
# backtrace to a pristine state. If you need to reconfigure an existing
-
# BacktraceCleaner so that it does not filter or modify the paths of any lines
-
# of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt>
-
# These two methods will give you a completely untouched backtrace.
-
#
-
# Inspired by the Quiet Backtrace gem by Thoughtbot.
-
2
class BacktraceCleaner
-
2
def initialize
-
2
@filters, @silencers = [], []
-
end
-
-
# Returns the backtrace after all filters and silencers have been run
-
# against it. Filters run first, then silencers.
-
2
def clean(backtrace, kind = :silent)
-
4
filtered = filter_backtrace(backtrace)
-
-
4
case kind
-
when :silent
-
4
silence(filtered)
-
when :noise
-
noise(filtered)
-
else
-
filtered
-
end
-
end
-
2
alias :filter :clean
-
-
# Adds a filter from the block provided. Each line in the backtrace will be
-
# mapped against this filter.
-
#
-
# # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
-
# backtrace_cleaner.add_filter { |line| line.gsub(Rails.root, '') }
-
2
def add_filter(&block)
-
8
@filters << block
-
end
-
-
# Adds a silencer from the block provided. If the silencer returns +true+
-
# for a given line, it will be excluded from the clean backtrace.
-
#
-
# # Will reject all lines that include the word "mongrel", like "/gems/mongrel/server.rb" or "/app/my_mongrel_server/rb"
-
# backtrace_cleaner.add_silencer { |line| line =~ /mongrel/ }
-
2
def add_silencer(&block)
-
2
@silencers << block
-
end
-
-
# Removes all silencers, but leaves in the filters. Useful if your
-
# context of debugging suddenly expands as you suspect a bug in one of
-
# the libraries you use.
-
2
def remove_silencers!
-
@silencers = []
-
end
-
-
# Removes all filters, but leaves in the silencers. Useful if you suddenly
-
# need to see entire filepaths in the backtrace that you had already
-
# filtered out.
-
2
def remove_filters!
-
@filters = []
-
end
-
-
2
private
-
2
def filter_backtrace(backtrace)
-
4
@filters.each do |f|
-
680
backtrace = backtrace.map { |line| f.call(line) }
-
end
-
-
4
backtrace
-
end
-
-
2
def silence(backtrace)
-
4
@silencers.each do |s|
-
170
backtrace = backtrace.reject { |line| s.call(line) }
-
end
-
-
4
backtrace
-
end
-
-
2
def noise(backtrace)
-
backtrace - silence(backtrace)
-
end
-
end
-
end
-
2
require 'active_support/core_ext/marshal'
-
2
require 'active_support/core_ext/file/atomic'
-
2
require 'active_support/core_ext/string/conversions'
-
2
require 'uri/common'
-
-
2
module ActiveSupport
-
2
module Cache
-
# A cache store implementation which stores everything on the filesystem.
-
#
-
# FileStore implements the Strategy::LocalCache strategy which implements
-
# an in-memory cache inside of a block.
-
2
class FileStore < Store
-
2
attr_reader :cache_path
-
-
2
DIR_FORMATTER = "%03X"
-
2
FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
-
2
FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
-
2
EXCLUDED_DIRS = ['.', '..'].freeze
-
-
2
def initialize(cache_path, options = nil)
-
2
super(options)
-
2
@cache_path = cache_path.to_s
-
2
extend Strategy::LocalCache
-
end
-
-
# Deletes all items from the cache. In this case it deletes all the entries in the specified
-
# file store directory except for .gitkeep. Be careful which directory is specified in your
-
# config file when using +FileStore+ because everything in that directory will be deleted.
-
2
def clear(options = nil)
-
root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)}
-
FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
-
end
-
-
# Preemptively iterates through all stored keys and removes the ones which have expired.
-
2
def cleanup(options = nil)
-
options = merged_options(options)
-
search_dir(cache_path) do |fname|
-
key = file_path_key(fname)
-
entry = read_entry(key, options)
-
delete_entry(key, options) if entry && entry.expired?
-
end
-
end
-
-
# Increments an already existing integer value that is stored in the cache.
-
# If the key is not found nothing is done.
-
2
def increment(name, amount = 1, options = nil)
-
modify_value(name, amount, options)
-
end
-
-
# Decrements an already existing integer value that is stored in the cache.
-
# If the key is not found nothing is done.
-
2
def decrement(name, amount = 1, options = nil)
-
modify_value(name, -amount, options)
-
end
-
-
2
def delete_matched(matcher, options = nil)
-
options = merged_options(options)
-
instrument(:delete_matched, matcher.inspect) do
-
matcher = key_matcher(matcher, options)
-
search_dir(cache_path) do |path|
-
key = file_path_key(path)
-
delete_entry(key, options) if key.match(matcher)
-
end
-
end
-
end
-
-
2
protected
-
-
2
def read_entry(key, options)
-
file_name = key_file_path(key)
-
if File.exist?(file_name)
-
File.open(file_name) { |f| Marshal.load(f) }
-
end
-
rescue => e
-
logger.error("FileStoreError (#{e}): #{e.message}") if logger
-
nil
-
end
-
-
2
def write_entry(key, entry, options)
-
file_name = key_file_path(key)
-
return false if options[:unless_exist] && File.exist?(file_name)
-
ensure_cache_path(File.dirname(file_name))
-
File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
-
true
-
end
-
-
2
def delete_entry(key, options)
-
file_name = key_file_path(key)
-
if File.exist?(file_name)
-
begin
-
File.delete(file_name)
-
delete_empty_directories(File.dirname(file_name))
-
true
-
rescue => e
-
# Just in case the error was caused by another process deleting the file first.
-
raise e if File.exist?(file_name)
-
false
-
end
-
end
-
end
-
-
2
private
-
# Lock a file for a block so only one process can modify it at a time.
-
2
def lock_file(file_name, &block) # :nodoc:
-
if File.exist?(file_name)
-
File.open(file_name, 'r+') do |f|
-
begin
-
f.flock File::LOCK_EX
-
yield
-
ensure
-
f.flock File::LOCK_UN
-
end
-
end
-
else
-
yield
-
end
-
end
-
-
# Translate a key into a file path.
-
2
def key_file_path(key)
-
if key.size > FILEPATH_MAX_SIZE
-
key = Digest::MD5.hexdigest(key)
-
end
-
-
fname = URI.encode_www_form_component(key)
-
hash = Zlib.adler32(fname)
-
hash, dir_1 = hash.divmod(0x1000)
-
dir_2 = hash.modulo(0x1000)
-
fname_paths = []
-
-
# Make sure file name doesn't exceed file system limits.
-
begin
-
fname_paths << fname[0, FILENAME_MAX_SIZE]
-
fname = fname[FILENAME_MAX_SIZE..-1]
-
end until fname.blank?
-
-
File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
-
end
-
-
# Translate a file path into a key.
-
2
def file_path_key(path)
-
fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last
-
URI.decode_www_form_component(fname, Encoding::UTF_8)
-
end
-
-
# Delete empty directories in the cache.
-
2
def delete_empty_directories(dir)
-
return if File.realpath(dir) == File.realpath(cache_path)
-
if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty?
-
Dir.delete(dir) rescue nil
-
delete_empty_directories(File.dirname(dir))
-
end
-
end
-
-
# Make sure a file path's directories exist.
-
2
def ensure_cache_path(path)
-
FileUtils.makedirs(path) unless File.exist?(path)
-
end
-
-
2
def search_dir(dir, &callback)
-
return if !File.exist?(dir)
-
Dir.foreach(dir) do |d|
-
next if EXCLUDED_DIRS.include?(d)
-
name = File.join(dir, d)
-
if File.directory?(name)
-
search_dir(name, &callback)
-
else
-
callback.call name
-
end
-
end
-
end
-
-
# Modifies the amount of an already existing integer value that is stored in the cache.
-
# If the key is not found nothing is done.
-
2
def modify_value(name, amount, options)
-
file_name = key_file_path(namespaced_key(name, options))
-
-
lock_file(file_name) do
-
options = merged_options(options)
-
-
if num = read(name, options)
-
num = num.to_i + amount
-
write(name, num, options)
-
num
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/object/duplicable'
-
2
require 'active_support/core_ext/string/inflections'
-
2
require 'active_support/per_thread_registry'
-
-
2
module ActiveSupport
-
2
module Cache
-
2
module Strategy
-
# Caches that implement LocalCache will be backed by an in-memory cache for the
-
# duration of a block. Repeated calls to the cache for the same key will hit the
-
# in-memory cache for faster access.
-
2
module LocalCache
-
2
autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware'
-
-
# Class for storing and registering the local caches.
-
2
class LocalCacheRegistry # :nodoc:
-
2
extend ActiveSupport::PerThreadRegistry
-
-
2
def initialize
-
@registry = {}
-
end
-
-
2
def cache_for(local_cache_key)
-
@registry[local_cache_key]
-
end
-
-
2
def set_cache_for(local_cache_key, value)
-
@registry[local_cache_key] = value
-
end
-
-
2
def self.set_cache_for(l, v); instance.set_cache_for l, v; end
-
2
def self.cache_for(l); instance.cache_for l; end
-
end
-
-
# Simple memory backed cache. This cache is not thread safe and is intended only
-
# for serving as a temporary memory cache for a single thread.
-
2
class LocalStore < Store
-
2
def initialize
-
super
-
@data = {}
-
end
-
-
# Don't allow synchronizing since it isn't thread safe,
-
2
def synchronize # :nodoc:
-
yield
-
end
-
-
2
def clear(options = nil)
-
@data.clear
-
end
-
-
2
def read_entry(key, options)
-
@data[key]
-
end
-
-
2
def write_entry(key, value, options)
-
@data[key] = value
-
true
-
end
-
-
2
def delete_entry(key, options)
-
!!@data.delete(key)
-
end
-
end
-
-
# Use a local cache for the duration of block.
-
2
def with_local_cache
-
use_temporary_local_cache(LocalStore.new) { yield }
-
end
-
# Middleware class can be inserted as a Rack handler to be local cache for the
-
# duration of request.
-
2
def middleware
-
@middleware ||= Middleware.new(
-
"ActiveSupport::Cache::Strategy::LocalCache",
-
2
local_cache_key)
-
end
-
-
2
def clear(options = nil) # :nodoc:
-
local_cache.clear(options) if local_cache
-
super
-
end
-
-
2
def cleanup(options = nil) # :nodoc:
-
local_cache.clear(options) if local_cache
-
super
-
end
-
-
2
def increment(name, amount = 1, options = nil) # :nodoc:
-
value = bypass_local_cache{super}
-
set_cache_value(value, name, amount, options)
-
value
-
end
-
-
2
def decrement(name, amount = 1, options = nil) # :nodoc:
-
value = bypass_local_cache{super}
-
set_cache_value(value, name, amount, options)
-
value
-
end
-
-
2
protected
-
2
def read_entry(key, options) # :nodoc:
-
if local_cache
-
entry = local_cache.read_entry(key, options)
-
unless entry
-
entry = super
-
local_cache.write_entry(key, entry, options)
-
end
-
entry
-
else
-
super
-
end
-
end
-
-
2
def write_entry(key, entry, options) # :nodoc:
-
local_cache.write_entry(key, entry, options) if local_cache
-
super
-
end
-
-
2
def delete_entry(key, options) # :nodoc:
-
local_cache.delete_entry(key, options) if local_cache
-
super
-
end
-
-
2
def set_cache_value(value, name, amount, options)
-
if local_cache
-
local_cache.mute do
-
if value
-
local_cache.write(name, value, options)
-
else
-
local_cache.delete(name, options)
-
end
-
end
-
end
-
end
-
-
2
private
-
-
2
def local_cache_key
-
2
@local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
-
end
-
-
2
def local_cache
-
LocalCacheRegistry.cache_for(local_cache_key)
-
end
-
-
2
def bypass_local_cache
-
use_temporary_local_cache(nil) { yield }
-
end
-
-
2
def use_temporary_local_cache(temporary_cache)
-
save_cache = LocalCacheRegistry.cache_for(local_cache_key)
-
begin
-
LocalCacheRegistry.set_cache_for(local_cache_key, temporary_cache)
-
yield
-
ensure
-
LocalCacheRegistry.set_cache_for(local_cache_key, save_cache)
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'rack/body_proxy'
-
2
require 'rack/utils'
-
-
2
module ActiveSupport
-
2
module Cache
-
2
module Strategy
-
2
module LocalCache
-
-
#--
-
# This class wraps up local storage for middlewares. Only the middleware method should
-
# construct them.
-
2
class Middleware # :nodoc:
-
2
attr_reader :name, :local_cache_key
-
-
2
def initialize(name, local_cache_key)
-
2
@name = name
-
2
@local_cache_key = local_cache_key
-
2
@app = nil
-
end
-
-
2
def new(app)
-
2
@app = app
-
2
self
-
end
-
-
2
def call(env)
-
LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
-
response = @app.call(env)
-
response[2] = ::Rack::BodyProxy.new(response[2]) do
-
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
-
end
-
response
-
rescue Rack::Utils::InvalidParameterError
-
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
-
[400, {}, []]
-
rescue Exception
-
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
-
raise
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/concern'
-
2
require 'active_support/ordered_options'
-
2
require 'active_support/core_ext/array/extract_options'
-
-
2
module ActiveSupport
-
# Configurable provides a <tt>config</tt> method to store and retrieve
-
# configuration options as an <tt>OrderedHash</tt>.
-
2
module Configurable
-
2
extend ActiveSupport::Concern
-
-
2
class Configuration < ActiveSupport::InheritableOptions
-
2
def compile_methods!
-
4
self.class.compile_methods!(keys)
-
end
-
-
# Compiles reader methods so we don't have to go through method_missing.
-
2
def self.compile_methods!(keys)
-
42
keys.reject { |m| method_defined?(m) }.each do |key|
-
26
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{key}; _get(#{key.inspect}); end
-
RUBY
-
end
-
end
-
end
-
-
2
module ClassMethods
-
2
def config
-
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
-
11
superclass.config.inheritable_copy
-
else
-
# create a new "anonymous" class that will host the compiled reader methods
-
2
Class.new(Configuration).new
-
485
end
-
end
-
-
2
def configure
-
yield config
-
end
-
-
# Allows you to add shortcut so that you don't have to refer to attribute
-
# through config. Also look at the example for config to contrast.
-
#
-
# Defines both class and instance config accessors.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :allowed_access
-
# end
-
#
-
# User.allowed_access # => nil
-
# User.allowed_access = false
-
# User.allowed_access # => false
-
#
-
# user = User.new
-
# user.allowed_access # => false
-
# user.allowed_access = true
-
# user.allowed_access # => true
-
#
-
# User.allowed_access # => false
-
#
-
# The attribute name must be a valid method name in Ruby.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :"1_Badname"
-
# end
-
# # => NameError: invalid config attribute name
-
#
-
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
-
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :allowed_access, instance_reader: false, instance_writer: false
-
# end
-
#
-
# User.allowed_access = false
-
# User.allowed_access # => false
-
#
-
# User.new.allowed_access = true # => NoMethodError
-
# User.new.allowed_access # => NoMethodError
-
#
-
# Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :allowed_access, instance_accessor: false
-
# end
-
#
-
# User.allowed_access = false
-
# User.allowed_access # => false
-
#
-
# User.new.allowed_access = true # => NoMethodError
-
# User.new.allowed_access # => NoMethodError
-
#
-
# Also you can pass a block to set up the attribute with a default value.
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# config_accessor :hair_colors do
-
# [:brown, :black, :blonde, :red]
-
# end
-
# end
-
#
-
# User.hair_colors # => [:brown, :black, :blonde, :red]
-
2
def config_accessor(*names)
-
22
options = names.extract_options!
-
-
22
names.each do |name|
-
42
raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/
-
-
42
reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
-
42
writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
-
-
42
singleton_class.class_eval reader, __FILE__, reader_line
-
42
singleton_class.class_eval writer, __FILE__, writer_line
-
-
42
unless options[:instance_accessor] == false
-
42
class_eval reader, __FILE__, reader_line unless options[:instance_reader] == false
-
42
class_eval writer, __FILE__, writer_line unless options[:instance_writer] == false
-
end
-
42
send("#{name}=", yield) if block_given?
-
end
-
end
-
end
-
-
# Reads and writes attributes from a configuration <tt>OrderedHash</tt>.
-
#
-
# require 'active_support/configurable'
-
#
-
# class User
-
# include ActiveSupport::Configurable
-
# end
-
#
-
# user = User.new
-
#
-
# user.config.allowed_access = true
-
# user.config.level = 1
-
#
-
# user.config.allowed_access # => true
-
# user.config.level # => 1
-
2
def config
-
98
@_config ||= self.class.config.inheritable_copy
-
end
-
end
-
end
-
-
2
Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path|
-
48
require path
-
end
-
2
require 'active_support/core_ext/big_decimal/conversions'
-
2
require 'securerandom'
-
-
2
module Digest
-
2
module UUID
-
2
DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
2
URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
2
OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
2
X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
-
-
# Generates a v5 non-random UUID (Universally Unique IDentifier).
-
#
-
# Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
-
# uuid_from_hash always generates the same UUID for a given name and namespace combination.
-
#
-
# See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt
-
2
def self.uuid_from_hash(hash_class, uuid_namespace, name)
-
if hash_class == Digest::MD5
-
version = 3
-
elsif hash_class == Digest::SHA1
-
version = 5
-
else
-
raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
-
end
-
-
hash = hash_class.new
-
hash.update(uuid_namespace)
-
hash.update(name)
-
-
ary = hash.digest.unpack('NnnnnN')
-
ary[2] = (ary[2] & 0x0FFF) | (version << 12)
-
ary[3] = (ary[3] & 0x3FFF) | 0x8000
-
-
"%08x-%04x-%04x-%04x-%04x%08x" % ary
-
end
-
-
# Convenience method for uuid_from_hash using Digest::MD5.
-
2
def self.uuid_v3(uuid_namespace, name)
-
self.uuid_from_hash(Digest::MD5, uuid_namespace, name)
-
end
-
-
# Convenience method for uuid_from_hash using Digest::SHA1.
-
2
def self.uuid_v5(uuid_namespace, name)
-
self.uuid_from_hash(Digest::SHA1, uuid_namespace, name)
-
end
-
-
# Convenience method for SecureRandom.uuid.
-
2
def self.uuid_v4
-
SecureRandom.uuid
-
end
-
end
-
end
-
2
require 'active_support/core_ext/file/atomic'
-
2
require 'fileutils'
-
-
2
class File
-
# Write to a file atomically. Useful for situations where you don't
-
# want other processes or threads to see half-written files.
-
#
-
# File.atomic_write('important.file') do |file|
-
# file.write('hello')
-
# end
-
#
-
# If your temp directory is not on the same filesystem as the file you're
-
# trying to write, you can provide a different temporary directory.
-
#
-
# File.atomic_write('/data/something.important', '/data/tmp') do |file|
-
# file.write('hello')
-
# end
-
2
def self.atomic_write(file_name, temp_dir = Dir.tmpdir)
-
require 'tempfile' unless defined?(Tempfile)
-
require 'fileutils' unless defined?(FileUtils)
-
-
temp_file = Tempfile.new(basename(file_name), temp_dir)
-
temp_file.binmode
-
yield temp_file
-
temp_file.close
-
-
if File.exist?(file_name)
-
# Get original file permissions
-
old_stat = stat(file_name)
-
else
-
# If not possible, probe which are the default permissions in the
-
# destination directory.
-
old_stat = probe_stat_in(dirname(file_name))
-
end
-
-
# Overwrite original file with temp file
-
FileUtils.mv(temp_file.path, file_name)
-
-
# Set correct permissions on new file
-
begin
-
chown(old_stat.uid, old_stat.gid, file_name)
-
# This operation will affect filesystem ACL's
-
chmod(old_stat.mode, file_name)
-
rescue Errno::EPERM, Errno::EACCES
-
# Changing file ownership failed, moving on.
-
end
-
end
-
-
# Private utility method.
-
2
def self.probe_stat_in(dir) #:nodoc:
-
basename = [
-
'.permissions_check',
-
Thread.current.object_id,
-
Process.pid,
-
rand(1000000)
-
].join('.')
-
-
file_name = join(dir, basename)
-
FileUtils.touch(file_name)
-
stat(file_name)
-
ensure
-
FileUtils.rm_f(file_name) if file_name
-
end
-
end
-
2
require 'active_support/core_ext/hash/compact'
-
2
require 'active_support/core_ext/hash/conversions'
-
2
require 'active_support/core_ext/hash/deep_merge'
-
2
require 'active_support/core_ext/hash/except'
-
2
require 'active_support/core_ext/hash/indifferent_access'
-
2
require 'active_support/core_ext/hash/keys'
-
2
require 'active_support/core_ext/hash/reverse_merge'
-
2
require 'active_support/core_ext/hash/slice'
-
2
require 'active_support/core_ext/hash/transform_values'
-
2
class Hash
-
# Returns a hash with non +nil+ values.
-
#
-
# hash = { a: true, b: false, c: nil}
-
# hash.compact # => { a: true, b: false}
-
# hash # => { a: true, b: false, c: nil}
-
# { c: nil }.compact # => {}
-
2
def compact
-
self.select { |_, value| !value.nil? }
-
end
-
-
# Replaces current hash with non +nil+ values.
-
#
-
# hash = { a: true, b: false, c: nil}
-
# hash.compact! # => { a: true, b: false}
-
# hash # => { a: true, b: false}
-
2
def compact!
-
self.reject! { |_, value| value.nil? }
-
end
-
end
-
2
class Hash
-
# Returns a new hash with the results of running +block+ once for every value.
-
# The keys are unchanged.
-
#
-
# { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
-
# # => { a: 2, b: 4, c: 6 }
-
2
def transform_values
-
38
return enum_for(:transform_values) unless block_given?
-
38
result = self.class.new
-
38
each do |key, value|
-
142
result[key] = yield(value)
-
end
-
38
result
-
end
-
-
# Destructive +transform_values+
-
2
def transform_values!
-
return enum_for(:transform_values!) unless block_given?
-
each do |key, value|
-
self[key] = yield(value)
-
end
-
end
-
end
-
2
require 'active_support/core_ext/integer/multiple'
-
2
require 'active_support/core_ext/integer/inflections'
-
2
require 'active_support/core_ext/integer/time'
-
2
require 'active_support/inflector'
-
-
2
class Integer
-
# Ordinalize turns a number into an ordinal string used to denote the
-
# position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
-
#
-
# 1.ordinalize # => "1st"
-
# 2.ordinalize # => "2nd"
-
# 1002.ordinalize # => "1002nd"
-
# 1003.ordinalize # => "1003rd"
-
# -11.ordinalize # => "-11th"
-
# -1001.ordinalize # => "-1001st"
-
2
def ordinalize
-
ActiveSupport::Inflector.ordinalize(self)
-
end
-
-
# Ordinal returns the suffix used to denote the position
-
# in an ordered sequence such as 1st, 2nd, 3rd, 4th.
-
#
-
# 1.ordinal # => "st"
-
# 2.ordinal # => "nd"
-
# 1002.ordinal # => "nd"
-
# 1003.ordinal # => "rd"
-
# -11.ordinal # => "th"
-
# -1001.ordinal # => "st"
-
2
def ordinal
-
ActiveSupport::Inflector.ordinal(self)
-
end
-
end
-
2
class Integer
-
# Check whether the integer is evenly divisible by the argument.
-
#
-
# 0.multiple_of?(0) # => true
-
# 6.multiple_of?(5) # => false
-
# 10.multiple_of?(2) # => true
-
2
def multiple_of?(number)
-
number != 0 ? self % number == 0 : zero?
-
end
-
end
-
2
require 'active_support/core_ext/kernel/agnostics'
-
2
require 'active_support/core_ext/kernel/concern'
-
2
require 'active_support/core_ext/kernel/debugger' if RUBY_VERSION < '2.0.0'
-
2
require 'active_support/core_ext/kernel/reporting'
-
2
require 'active_support/core_ext/kernel/singleton_class'
-
2
class Object
-
# Makes backticks behave (somewhat more) similarly on all platforms.
-
# On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
-
# spawned shell prints a message to stderr and sets $?. We emulate
-
# Unix on the former but not the latter.
-
2
def `(command) #:nodoc:
-
super
-
rescue Errno::ENOENT => e
-
STDERR.puts "#$0: #{e}"
-
end
-
end
-
2
require 'active_support/core_ext/module/concerning'
-
-
2
module Kernel
-
# A shortcut to define a toplevel concern, not within a module.
-
#
-
# See Module::Concerning for more.
-
2
def concern(topic, &module_definition)
-
Object.concern topic, &module_definition
-
end
-
end
-
2
require 'active_support/core_ext/module/aliasing'
-
-
2
module Marshal
-
2
class << self
-
2
def load_with_autoloading(source)
-
132
load_without_autoloading(source)
-
rescue ArgumentError, NameError => exc
-
if exc.message.match(%r|undefined class/module (.+)|)
-
# try loading the class/module
-
$1.constantize
-
# if it is a IO we need to go back to read the object
-
source.rewind if source.respond_to?(:rewind)
-
retry
-
else
-
raise exc
-
end
-
end
-
-
2
alias_method_chain :load, :autoloading
-
end
-
end
-
2
class Module
-
###
-
# TODO: remove this after 1.9 support is dropped
-
2
def methods_transplantable? # :nodoc:
-
4
x = Module.new {
-
4
def foo; end # :nodoc:
-
}
-
8
Module.new { define_method :bar, x.instance_method(:foo) }
-
4
true
-
rescue TypeError
-
false
-
end
-
end
-
2
require 'active_support/core_ext/numeric/bytes'
-
2
require 'active_support/core_ext/numeric/time'
-
2
require 'active_support/core_ext/numeric/conversions'
-
2
require 'active_support/core_ext/big_decimal/conversions'
-
2
require 'active_support/number_helper'
-
-
2
class Numeric
-
-
# Provides options for converting numbers into formatted strings.
-
# Options are provided for phone numbers, currency, percentage,
-
# precision, positional notation, file size and pretty printing.
-
#
-
# ==== Options
-
#
-
# For details on which formats use which options, see ActiveSupport::NumberHelper
-
#
-
# ==== Examples
-
#
-
# Phone Numbers:
-
# 5551234.to_s(:phone) # => 555-1234
-
# 1235551234.to_s(:phone) # => 123-555-1234
-
# 1235551234.to_s(:phone, area_code: true) # => (123) 555-1234
-
# 1235551234.to_s(:phone, delimiter: ' ') # => 123 555 1234
-
# 1235551234.to_s(:phone, area_code: true, extension: 555) # => (123) 555-1234 x 555
-
# 1235551234.to_s(:phone, country_code: 1) # => +1-123-555-1234
-
# 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
-
# # => +1.123.555.1234 x 1343
-
#
-
# Currency:
-
# 1234567890.50.to_s(:currency) # => $1,234,567,890.50
-
# 1234567890.506.to_s(:currency) # => $1,234,567,890.51
-
# 1234567890.506.to_s(:currency, precision: 3) # => $1,234,567,890.506
-
# 1234567890.506.to_s(:currency, locale: :fr) # => 1 234 567 890,51 €
-
# -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
-
# # => ($1,234,567,890.50)
-
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '')
-
# # => £1234567890,50
-
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u')
-
# # => 1234567890,50 £
-
#
-
# Percentage:
-
# 100.to_s(:percentage) # => 100.000%
-
# 100.to_s(:percentage, precision: 0) # => 100%
-
# 1000.to_s(:percentage, delimiter: '.', separator: ',') # => 1.000,000%
-
# 302.24398923423.to_s(:percentage, precision: 5) # => 302.24399%
-
# 1000.to_s(:percentage, locale: :fr) # => 1 000,000%
-
# 100.to_s(:percentage, format: '%n %') # => 100.000 %
-
#
-
# Delimited:
-
# 12345678.to_s(:delimited) # => 12,345,678
-
# 12345678.05.to_s(:delimited) # => 12,345,678.05
-
# 12345678.to_s(:delimited, delimiter: '.') # => 12.345.678
-
# 12345678.to_s(:delimited, delimiter: ',') # => 12,345,678
-
# 12345678.05.to_s(:delimited, separator: ' ') # => 12,345,678 05
-
# 12345678.05.to_s(:delimited, locale: :fr) # => 12 345 678,05
-
# 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
-
# # => 98 765 432,98
-
#
-
# Rounded:
-
# 111.2345.to_s(:rounded) # => 111.235
-
# 111.2345.to_s(:rounded, precision: 2) # => 111.23
-
# 13.to_s(:rounded, precision: 5) # => 13.00000
-
# 389.32314.to_s(:rounded, precision: 0) # => 389
-
# 111.2345.to_s(:rounded, significant: true) # => 111
-
# 111.2345.to_s(:rounded, precision: 1, significant: true) # => 100
-
# 13.to_s(:rounded, precision: 5, significant: true) # => 13.000
-
# 111.234.to_s(:rounded, locale: :fr) # => 111,234
-
# 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
-
# # => 13
-
# 389.32314.to_s(:rounded, precision: 4, significant: true) # => 389.3
-
# 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
-
# # => 1.111,23
-
#
-
# Human-friendly size in Bytes:
-
# 123.to_s(:human_size) # => 123 Bytes
-
# 1234.to_s(:human_size) # => 1.21 KB
-
# 12345.to_s(:human_size) # => 12.1 KB
-
# 1234567.to_s(:human_size) # => 1.18 MB
-
# 1234567890.to_s(:human_size) # => 1.15 GB
-
# 1234567890123.to_s(:human_size) # => 1.12 TB
-
# 1234567.to_s(:human_size, precision: 2) # => 1.2 MB
-
# 483989.to_s(:human_size, precision: 2) # => 470 KB
-
# 1234567.to_s(:human_size, precision: 2, separator: ',') # => 1,2 MB
-
# 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
-
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
-
#
-
# Human-friendly format:
-
# 123.to_s(:human) # => "123"
-
# 1234.to_s(:human) # => "1.23 Thousand"
-
# 12345.to_s(:human) # => "12.3 Thousand"
-
# 1234567.to_s(:human) # => "1.23 Million"
-
# 1234567890.to_s(:human) # => "1.23 Billion"
-
# 1234567890123.to_s(:human) # => "1.23 Trillion"
-
# 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
-
# 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
-
# 489939.to_s(:human, precision: 2) # => "490 Thousand"
-
# 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
-
# 1234567.to_s(:human, precision: 4,
-
# significant: false) # => "1.2346 Million"
-
# 1234567.to_s(:human, precision: 1,
-
# separator: ',',
-
# significant: false) # => "1,2 Million"
-
2
def to_formatted_s(format = :default, options = {})
-
case format
-
when :phone
-
return ActiveSupport::NumberHelper.number_to_phone(self, options)
-
when :currency
-
return ActiveSupport::NumberHelper.number_to_currency(self, options)
-
when :percentage
-
return ActiveSupport::NumberHelper.number_to_percentage(self, options)
-
when :delimited
-
return ActiveSupport::NumberHelper.number_to_delimited(self, options)
-
when :rounded
-
return ActiveSupport::NumberHelper.number_to_rounded(self, options)
-
when :human
-
return ActiveSupport::NumberHelper.number_to_human(self, options)
-
when :human_size
-
return ActiveSupport::NumberHelper.number_to_human_size(self, options)
-
else
-
self.to_default_s
-
end
-
end
-
-
2
[Float, Fixnum, Bignum, BigDecimal].each do |klass|
-
8
klass.send(:alias_method, :to_default_s, :to_s)
-
-
8
klass.send(:define_method, :to_s) do |*args|
-
1251
if args[0].is_a?(Symbol)
-
format = args[0]
-
options = args[1] || {}
-
-
self.to_formatted_s(format, options)
-
else
-
1251
to_default_s(*args)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/range/conversions'
-
2
require 'active_support/core_ext/range/include_range'
-
2
require 'active_support/core_ext/range/overlaps'
-
2
require 'active_support/core_ext/range/each'
-
2
class Range
-
2
RANGE_FORMATS = {
-
:db => Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" }
-
}
-
-
# Gives a human readable format of the range.
-
#
-
# (1..100).to_formatted_s # => "1..100"
-
2
def to_formatted_s(format = :default)
-
if formatter = RANGE_FORMATS[format]
-
formatter.call(first, last)
-
else
-
to_default_s
-
end
-
end
-
-
2
alias_method :to_default_s, :to_s
-
2
alias_method :to_s, :to_formatted_s
-
end
-
2
require 'active_support/core_ext/module/aliasing'
-
-
2
class Range #:nodoc:
-
-
2
def each_with_time_with_zone(&block)
-
6
ensure_iteration_allowed
-
6
each_without_time_with_zone(&block)
-
end
-
2
alias_method_chain :each, :time_with_zone
-
-
2
def step_with_time_with_zone(n = 1, &block)
-
ensure_iteration_allowed
-
step_without_time_with_zone(n, &block)
-
end
-
2
alias_method_chain :step, :time_with_zone
-
-
2
private
-
2
def ensure_iteration_allowed
-
6
if first.is_a?(Time)
-
raise TypeError, "can't iterate from #{first.class}"
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/aliasing'
-
-
2
class Range
-
# Extends the default Range#include? to support range comparisons.
-
# (1..5).include?(1..5) # => true
-
# (1..5).include?(2..3) # => true
-
# (1..5).include?(2..6) # => false
-
#
-
# The native Range#include? behavior is untouched.
-
# ('a'..'f').include?('c') # => true
-
# (5..9).include?(11) # => false
-
2
def include_with_range?(value)
-
if value.is_a?(::Range)
-
# 1...10 includes 1..9 but it does not include 1..10.
-
operator = exclude_end? && !value.exclude_end? ? :< : :<=
-
include_without_range?(value.first) && value.last.send(operator, last)
-
else
-
include_without_range?(value)
-
end
-
end
-
-
2
alias_method_chain :include?, :range
-
end
-
2
class Range
-
# Compare two ranges and see if they overlap each other
-
# (1..5).overlaps?(4..6) # => true
-
# (1..5).overlaps?(7..9) # => false
-
2
def overlaps?(other)
-
cover?(other.first) || other.cover?(first)
-
end
-
end
-
2
require 'active_support/core_ext/string/conversions'
-
2
require 'active_support/core_ext/string/filters'
-
2
require 'active_support/core_ext/string/multibyte'
-
2
require 'active_support/core_ext/string/starts_ends_with'
-
2
require 'active_support/core_ext/string/inflections'
-
2
require 'active_support/core_ext/string/access'
-
2
require 'active_support/core_ext/string/behavior'
-
2
require 'active_support/core_ext/string/output_safety'
-
2
require 'active_support/core_ext/string/exclude'
-
2
require 'active_support/core_ext/string/strip'
-
2
require 'active_support/core_ext/string/inquiry'
-
2
require 'active_support/core_ext/string/indent'
-
2
require 'active_support/core_ext/string/zones'
-
2
class String
-
# Enable more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
-
2
def acts_like_string?
-
true
-
end
-
end
-
2
class String
-
# The inverse of <tt>String#include?</tt>. Returns true if the string
-
# does not include the other string.
-
#
-
# "hello".exclude? "lo" # => false
-
# "hello".exclude? "ol" # => true
-
# "hello".exclude? ?h # => false
-
2
def exclude?(string)
-
!include?(string)
-
end
-
end
-
2
class String
-
# Same as +indent+, except it indents the receiver in-place.
-
#
-
# Returns the indented string, or +nil+ if there was nothing to indent.
-
2
def indent!(amount, indent_string=nil, indent_empty_lines=false)
-
indent_string = indent_string || self[/^[ \t]/] || ' '
-
re = indent_empty_lines ? /^/ : /^(?!$)/
-
gsub!(re, indent_string * amount)
-
end
-
-
# Indents the lines in the receiver:
-
#
-
# <<EOS.indent(2)
-
# def some_method
-
# some_code
-
# end
-
# EOS
-
# # =>
-
# def some_method
-
# some_code
-
# end
-
#
-
# The second argument, +indent_string+, specifies which indent string to
-
# use. The default is +nil+, which tells the method to make a guess by
-
# peeking at the first indented line, and fallback to a space if there is
-
# none.
-
#
-
# " foo".indent(2) # => " foo"
-
# "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
-
# "foo".indent(2, "\t") # => "\t\tfoo"
-
#
-
# While +indent_string+ is typically one space or tab, it may be any string.
-
#
-
# The third argument, +indent_empty_lines+, is a flag that says whether
-
# empty lines should be indented. Default is false.
-
#
-
# "foo\n\nbar".indent(2) # => " foo\n\n bar"
-
# "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar"
-
#
-
2
def indent(amount, indent_string=nil, indent_empty_lines=false)
-
dup.tap {|_| _.indent!(amount, indent_string, indent_empty_lines)}
-
end
-
end
-
2
require 'active_support/string_inquirer'
-
-
2
class String
-
# Wraps the current string in the <tt>ActiveSupport::StringInquirer</tt> class,
-
# which gives you a prettier way to test for equality.
-
#
-
# env = 'production'.inquiry
-
# env.production? # => true
-
# env.development? # => false
-
2
def inquiry
-
ActiveSupport::StringInquirer.new(self)
-
end
-
end
-
# Backport of Struct#to_h from Ruby 2.0
-
class Struct # :nodoc:
-
def to_h
-
Hash[members.zip(values)]
-
end
-
2
end unless Struct.instance_methods.include?(:to_h)
-
class Thread
-
LOCK = Mutex.new # :nodoc:
-
-
# Returns the value of a thread local variable that has been set. Note that
-
# these are different than fiber local values.
-
#
-
# Thread local values are carried along with threads, and do not respect
-
# fibers. For example:
-
#
-
# Thread.new {
-
# Thread.current.thread_variable_set("foo", "bar") # set a thread local
-
# Thread.current["foo"] = "bar" # set a fiber local
-
#
-
# Fiber.new {
-
# Fiber.yield [
-
# Thread.current.thread_variable_get("foo"), # get the thread local
-
# Thread.current["foo"], # get the fiber local
-
# ]
-
# }.resume
-
# }.join.value # => ['bar', nil]
-
#
-
# The value <tt>"bar"</tt> is returned for the thread local, where +nil+ is returned
-
# for the fiber local. The fiber is executed in the same thread, so the
-
# thread local values are available.
-
def thread_variable_get(key)
-
_locals[key.to_sym]
-
end
-
-
# Sets a thread local with +key+ to +value+. Note that these are local to
-
# threads, and not to fibers. Please see Thread#thread_variable_get for
-
# more information.
-
def thread_variable_set(key, value)
-
_locals[key.to_sym] = value
-
end
-
-
# Returns an array of the names of the thread-local variables (as Symbols).
-
#
-
# thr = Thread.new do
-
# Thread.current.thread_variable_set(:cat, 'meow')
-
# Thread.current.thread_variable_set("dog", 'woof')
-
# end
-
# thr.join # => #<Thread:0x401b3f10 dead>
-
# thr.thread_variables # => [:dog, :cat]
-
#
-
# Note that these are not fiber local variables. Please see Thread#thread_variable_get
-
# for more details.
-
def thread_variables
-
_locals.keys
-
end
-
-
# Returns <tt>true</tt> if the given string (or symbol) exists as a
-
# thread-local variable.
-
#
-
# me = Thread.current
-
# me.thread_variable_set(:oliver, "a")
-
# me.thread_variable?(:oliver) # => true
-
# me.thread_variable?(:stanley) # => false
-
#
-
# Note that these are not fiber local variables. Please see Thread#thread_variable_get
-
# for more details.
-
def thread_variable?(key)
-
_locals.has_key?(key.to_sym)
-
end
-
-
# Freezes the thread so that thread local variables cannot be set via
-
# Thread#thread_variable_set, nor can fiber local variables be set.
-
#
-
# me = Thread.current
-
# me.freeze
-
# me.thread_variable_set(:oliver, "a") #=> RuntimeError: can't modify frozen thread locals
-
# me[:oliver] = "a" #=> RuntimeError: can't modify frozen thread locals
-
def freeze
-
_locals.freeze
-
super
-
end
-
-
private
-
-
def _locals
-
if defined?(@_locals)
-
@_locals
-
else
-
LOCK.synchronize { @_locals ||= {} }
-
end
-
end
-
2
end unless Thread.instance_methods.include?(:thread_variable_set)
-
# encoding: utf-8
-
2
require 'active_support/json'
-
2
require 'active_support/core_ext/string/access'
-
2
require 'active_support/core_ext/string/behavior'
-
2
require 'active_support/core_ext/module/delegation'
-
-
2
module ActiveSupport #:nodoc:
-
2
module Multibyte #:nodoc:
-
# Chars enables you to work transparently with UTF-8 encoding in the Ruby
-
# String class without having extensive knowledge about the encoding. A
-
# Chars object accepts a string upon initialization and proxies String
-
# methods in an encoding safe manner. All the normal String methods are also
-
# implemented on the proxy.
-
#
-
# String methods are proxied through the Chars object, and can be accessed
-
# through the +mb_chars+ method. Methods which would normally return a
-
# String object now return a Chars object so methods can be chained.
-
#
-
# 'The Perfect String '.mb_chars.downcase.strip.normalize # => "the perfect string"
-
#
-
# Chars objects are perfectly interchangeable with String objects as long as
-
# no explicit class checks are made. If certain methods do explicitly check
-
# the class, call +to_s+ before you pass chars objects to them.
-
#
-
# bad.explicit_checking_method 'T'.mb_chars.downcase.to_s
-
#
-
# The default Chars implementation assumes that the encoding of the string
-
# is UTF-8, if you want to handle different encodings you can write your own
-
# multibyte string handler and configure it through
-
# ActiveSupport::Multibyte.proxy_class.
-
#
-
# class CharsForUTF32
-
# def size
-
# @wrapped_string.size / 4
-
# end
-
#
-
# def self.accepts?(string)
-
# string.length % 4 == 0
-
# end
-
# end
-
#
-
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
-
2
class Chars
-
2
include Comparable
-
2
attr_reader :wrapped_string
-
2
alias to_s wrapped_string
-
2
alias to_str wrapped_string
-
-
2
delegate :<=>, :=~, :acts_like_string?, :to => :wrapped_string
-
-
# Creates a new Chars instance by wrapping _string_.
-
2
def initialize(string)
-
@wrapped_string = string
-
@wrapped_string.force_encoding(Encoding::UTF_8) unless @wrapped_string.frozen?
-
end
-
-
# Forward all undefined methods to the wrapped string.
-
2
def method_missing(method, *args, &block)
-
result = @wrapped_string.__send__(method, *args, &block)
-
if method.to_s =~ /!$/
-
self if result
-
else
-
result.kind_of?(String) ? chars(result) : result
-
end
-
end
-
-
# Returns +true+ if _obj_ responds to the given method. Private methods
-
# are included in the search only if the optional second parameter
-
# evaluates to +true+.
-
2
def respond_to_missing?(method, include_private)
-
@wrapped_string.respond_to?(method, include_private)
-
end
-
-
# Returns +true+ when the proxy class can handle the string. Returns
-
# +false+ otherwise.
-
2
def self.consumes?(string)
-
string.encoding == Encoding::UTF_8
-
end
-
-
# Works just like <tt>String#split</tt>, with the exception that the items
-
# in the resulting list are Chars instances instead of String. This makes
-
# chaining methods easier.
-
#
-
# 'Café périferôl'.mb_chars.split(/é/).map { |part| part.upcase.to_s } # => ["CAF", " P", "RIFERÔL"]
-
2
def split(*args)
-
@wrapped_string.split(*args).map { |i| self.class.new(i) }
-
end
-
-
# Works like <tt>String#slice!</tt>, but returns an instance of
-
# Chars, or nil if the string was not modified.
-
2
def slice!(*args)
-
chars(@wrapped_string.slice!(*args))
-
end
-
-
# Reverses all characters in the string.
-
#
-
# 'Café'.mb_chars.reverse.to_s # => 'éfaC'
-
2
def reverse
-
chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack('U*'))
-
end
-
-
# Limits the byte size of the string to a number of bytes without breaking
-
# characters. Usable when the storage for a string is limited for some
-
# reason.
-
#
-
# 'こんにちは'.mb_chars.limit(7).to_s # => "こん"
-
2
def limit(limit)
-
slice(0...translate_offset(limit))
-
end
-
-
# Converts characters in the string to uppercase.
-
#
-
# 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?"
-
2
def upcase
-
chars Unicode.upcase(@wrapped_string)
-
end
-
-
# Converts characters in the string to lowercase.
-
#
-
# 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum"
-
2
def downcase
-
chars Unicode.downcase(@wrapped_string)
-
end
-
-
# Converts characters in the string to the opposite case.
-
#
-
# 'El Cañón".mb_chars.swapcase.to_s # => "eL cAÑÓN"
-
2
def swapcase
-
chars Unicode.swapcase(@wrapped_string)
-
end
-
-
# Converts the first character to uppercase and the remainder to lowercase.
-
#
-
# 'über'.mb_chars.capitalize.to_s # => "Über"
-
2
def capitalize
-
(slice(0) || chars('')).upcase + (slice(1..-1) || chars('')).downcase
-
end
-
-
# Capitalizes the first letter of every word, when possible.
-
#
-
# "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró"
-
# "日本語".mb_chars.titleize # => "日本語"
-
2
def titleize
-
chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1)})
-
end
-
2
alias_method :titlecase, :titleize
-
-
# Returns the KC normalization of the string by default. NFKC is
-
# considered the best normalization form for passing strings to databases
-
# and validations.
-
#
-
# * <tt>form</tt> - The form you want to normalize in. Should be one of the following:
-
# <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is
-
# ActiveSupport::Multibyte::Unicode.default_normalization_form
-
2
def normalize(form = nil)
-
chars(Unicode.normalize(@wrapped_string, form))
-
end
-
-
# Performs canonical decomposition on all the characters.
-
#
-
# 'é'.length # => 2
-
# 'é'.mb_chars.decompose.to_s.length # => 3
-
2
def decompose
-
chars(Unicode.decompose(:canonical, @wrapped_string.codepoints.to_a).pack('U*'))
-
end
-
-
# Performs composition on all the characters.
-
#
-
# 'é'.length # => 3
-
# 'é'.mb_chars.compose.to_s.length # => 2
-
2
def compose
-
chars(Unicode.compose(@wrapped_string.codepoints.to_a).pack('U*'))
-
end
-
-
# Returns the number of grapheme clusters in the string.
-
#
-
# 'क्षि'.mb_chars.length # => 4
-
# 'क्षि'.mb_chars.grapheme_length # => 3
-
2
def grapheme_length
-
Unicode.unpack_graphemes(@wrapped_string).length
-
end
-
-
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
-
# resulting in a valid UTF-8 string.
-
#
-
# Passing +true+ will forcibly tidy all bytes, assuming that the string's
-
# encoding is entirely CP1252 or ISO-8859-1.
-
2
def tidy_bytes(force = false)
-
chars(Unicode.tidy_bytes(@wrapped_string, force))
-
end
-
-
2
def as_json(options = nil) #:nodoc:
-
to_s.as_json(options)
-
end
-
-
2
%w(capitalize downcase reverse tidy_bytes upcase).each do |method|
-
10
define_method("#{method}!") do |*args|
-
@wrapped_string = send(method, *args).to_s
-
self
-
end
-
end
-
-
2
protected
-
-
2
def translate_offset(byte_offset) #:nodoc:
-
return nil if byte_offset.nil?
-
return 0 if @wrapped_string == ''
-
-
begin
-
@wrapped_string.byteslice(0...byte_offset).unpack('U*').length
-
rescue ArgumentError
-
byte_offset -= 1
-
retry
-
end
-
end
-
-
2
def chars(string) #:nodoc:
-
self.class.new(string)
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/module/delegation'
-
2
require 'active_support/core_ext/object/blank'
-
2
require 'logger'
-
2
require 'active_support/logger'
-
-
2
module ActiveSupport
-
# Wraps any standard Logger object to provide tagging capabilities.
-
#
-
# logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
-
# logger.tagged('BCX') { logger.info 'Stuff' } # Logs "[BCX] Stuff"
-
# logger.tagged('BCX', "Jason") { logger.info 'Stuff' } # Logs "[BCX] [Jason] Stuff"
-
# logger.tagged('BCX') { logger.tagged('Jason') { logger.info 'Stuff' } } # Logs "[BCX] [Jason] Stuff"
-
#
-
# This is used by the default Rails.logger as configured by Railties to make
-
# it easy to stamp log lines with subdomains, request ids, and anything else
-
# to aid debugging of multi-user production applications.
-
2
module TaggedLogging
-
2
module Formatter # :nodoc:
-
# This method is invoked when a log event occurs.
-
2
def call(severity, timestamp, progname, msg)
-
597
super(severity, timestamp, progname, "#{tags_text}#{msg}")
-
end
-
-
2
def tagged(*tags)
-
new_tags = push_tags(*tags)
-
yield self
-
ensure
-
pop_tags(new_tags.size)
-
end
-
-
2
def push_tags(*tags)
-
tags.flatten.reject(&:blank?).tap do |new_tags|
-
current_tags.concat new_tags
-
end
-
end
-
-
2
def pop_tags(size = 1)
-
current_tags.pop size
-
end
-
-
2
def clear_tags!
-
current_tags.clear
-
end
-
-
2
def current_tags
-
597
Thread.current[:activesupport_tagged_logging_tags] ||= []
-
end
-
-
2
private
-
2
def tags_text
-
597
tags = current_tags
-
597
if tags.any?
-
tags.collect { |tag| "[#{tag}] " }.join
-
end
-
end
-
end
-
-
2
def self.new(logger)
-
# Ensure we set a default formatter so we aren't extending nil!
-
2
logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new
-
2
logger.formatter.extend Formatter
-
2
logger.extend(self)
-
end
-
-
2
delegate :push_tags, :pop_tags, :clear_tags!, to: :formatter
-
-
2
def tagged(*tags)
-
formatter.tagged(*tags) { yield self }
-
end
-
-
2
def flush
-
clear_tags!
-
super if defined?(super)
-
end
-
end
-
end
-
2
gem 'minitest' # make sure we get the gem, not stdlib
-
2
require 'minitest'
-
2
require 'active_support/testing/tagged_logging'
-
2
require 'active_support/testing/setup_and_teardown'
-
2
require 'active_support/testing/assertions'
-
2
require 'active_support/testing/deprecation'
-
2
require 'active_support/testing/declarative'
-
2
require 'active_support/testing/isolation'
-
2
require 'active_support/testing/constant_lookup'
-
2
require 'active_support/testing/time_helpers'
-
2
require 'active_support/core_ext/kernel/reporting'
-
2
require 'active_support/deprecation'
-
-
2
module ActiveSupport
-
2
class TestCase < ::Minitest::Test
-
2
Assertion = Minitest::Assertion
-
-
2
class << self
-
# Sets the order in which test cases are run.
-
#
-
# ActiveSupport::TestCase.test_order = :random # => :random
-
#
-
# Valid values are:
-
# * +:random+ (to run tests in random order)
-
# * +:parallel+ (to run tests in parallel)
-
# * +:sorted+ (to run tests alphabetically by method name)
-
# * +:alpha+ (equivalent to +:sorted+)
-
2
def test_order=(new_order)
-
ActiveSupport.test_order = new_order
-
end
-
-
# Returns the order in which test cases are run.
-
#
-
# ActiveSupport::TestCase.test_order # => :sorted
-
#
-
# Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
-
# Defaults to +:sorted+.
-
2
def test_order
-
36
test_order = ActiveSupport.test_order
-
-
36
if test_order.nil?
-
ActiveSupport::Deprecation.warn "You did not specify a value for the " \
-
"configuration option `active_support.test_order`. In Rails 5, " \
-
"the default value of this option will change from `:sorted` to " \
-
"`:random`.\n" \
-
"To disable this warning and keep the current behavior, you can add " \
-
"the following line to your `config/environments/test.rb`:\n" \
-
"\n" \
-
" Rails.application.configure do\n" \
-
" config.active_support.test_order = :sorted\n" \
-
" end\n" \
-
"\n" \
-
"Alternatively, you can opt into the future behavior by setting this " \
-
"option to `:random`."
-
-
test_order = :sorted
-
self.test_order = test_order
-
end
-
-
36
test_order
-
end
-
-
2
alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent!
-
end
-
-
2
alias_method :method_name, :name
-
-
2
include ActiveSupport::Testing::TaggedLogging
-
2
include ActiveSupport::Testing::SetupAndTeardown
-
2
include ActiveSupport::Testing::Assertions
-
2
include ActiveSupport::Testing::Deprecation
-
2
include ActiveSupport::Testing::TimeHelpers
-
2
extend ActiveSupport::Testing::Declarative
-
-
# test/unit backwards compatibility methods
-
2
alias :assert_raise :assert_raises
-
2
alias :assert_not_empty :refute_empty
-
2
alias :assert_not_equal :refute_equal
-
2
alias :assert_not_in_delta :refute_in_delta
-
2
alias :assert_not_in_epsilon :refute_in_epsilon
-
2
alias :assert_not_includes :refute_includes
-
2
alias :assert_not_instance_of :refute_instance_of
-
2
alias :assert_not_kind_of :refute_kind_of
-
2
alias :assert_no_match :refute_match
-
2
alias :assert_not_nil :refute_nil
-
2
alias :assert_not_operator :refute_operator
-
2
alias :assert_not_predicate :refute_predicate
-
2
alias :assert_not_respond_to :refute_respond_to
-
2
alias :assert_not_same :refute_same
-
-
# Fails if the block raises an exception.
-
#
-
# assert_nothing_raised do
-
# ...
-
# end
-
2
def assert_nothing_raised(*args)
-
yield
-
end
-
end
-
end
-
2
require 'active_support/core_ext/object/blank'
-
-
2
module ActiveSupport
-
2
module Testing
-
2
module Assertions
-
# Assert that an expression is not truthy. Passes if <tt>object</tt> is
-
# +nil+ or +false+. "Truthy" means "considered true in a conditional"
-
# like <tt>if foo</tt>.
-
#
-
# assert_not nil # => true
-
# assert_not false # => true
-
# assert_not 'foo' # => Expected "foo" to be nil or false
-
#
-
# An error message can be specified.
-
#
-
# assert_not foo, 'foo should be false'
-
2
def assert_not(object, message = nil)
-
message ||= "Expected #{mu_pp(object)} to be nil or false"
-
assert !object, message
-
end
-
-
# Test numeric difference between the return value of an expression as a
-
# result of what is evaluated in the yielded block.
-
#
-
# assert_difference 'Article.count' do
-
# post :create, article: {...}
-
# end
-
#
-
# An arbitrary expression is passed in and evaluated.
-
#
-
# assert_difference 'assigns(:article).comments(:reload).size' do
-
# post :create, comment: {...}
-
# end
-
#
-
# An arbitrary positive or negative difference can be specified.
-
# The default is <tt>1</tt>.
-
#
-
# assert_difference 'Article.count', -1 do
-
# post :delete, id: ...
-
# end
-
#
-
# An array of expressions can also be passed in and evaluated.
-
#
-
# assert_difference [ 'Article.count', 'Post.count' ], 2 do
-
# post :create, article: {...}
-
# end
-
#
-
# A lambda or a list of lambdas can be passed in and evaluated:
-
#
-
# assert_difference ->{ Article.count }, 2 do
-
# post :create, article: {...}
-
# end
-
#
-
# assert_difference [->{ Article.count }, ->{ Post.count }], 2 do
-
# post :create, article: {...}
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
-
# post :delete, id: ...
-
# end
-
2
def assert_difference(expression, difference = 1, message = nil, &block)
-
9
expressions = Array(expression)
-
-
9
exps = expressions.map { |e|
-
27
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
-
}
-
18
before = exps.map { |e| e.call }
-
-
9
yield
-
-
9
expressions.zip(exps).each_with_index do |(code, e), i|
-
9
error = "#{code.inspect} didn't change by #{difference}"
-
9
error = "#{message}.\n#{error}" if message
-
9
assert_equal(before[i] + difference, e.call, error)
-
end
-
end
-
-
# Assertion that the numeric result of evaluating an expression is not
-
# changed before and after invoking the passed in block.
-
#
-
# assert_no_difference 'Article.count' do
-
# post :create, article: invalid_attributes
-
# end
-
#
-
# An error message can be specified.
-
#
-
# assert_no_difference 'Article.count', 'An Article should not be created' do
-
# post :create, article: invalid_attributes
-
# end
-
2
def assert_no_difference(expression, message = nil, &block)
-
2
assert_difference expression, 0, message, &block
-
end
-
end
-
end
-
end
-
2
gem 'minitest'
-
-
2
require 'minitest'
-
-
2
Minitest.autorun
-
2
require "active_support/concern"
-
2
require "active_support/inflector"
-
-
2
module ActiveSupport
-
2
module Testing
-
# Resolves a constant from a minitest spec name.
-
#
-
# Given the following spec-style test:
-
#
-
# describe WidgetsController, :index do
-
# describe "authenticated user" do
-
# describe "returns widgets" do
-
# it "has a controller that exists" do
-
# assert_kind_of WidgetsController, @controller
-
# end
-
# end
-
# end
-
# end
-
#
-
# The test will have the following name:
-
#
-
# "WidgetsController::index::authenticated user::returns widgets"
-
#
-
# The constant WidgetsController can be resolved from the name.
-
# The following code will resolve the constant:
-
#
-
# controller = determine_constant_from_test_name(name) do |constant|
-
# Class === constant && constant < ::ActionController::Metal
-
# end
-
2
module ConstantLookup
-
2
extend ::ActiveSupport::Concern
-
-
2
module ClassMethods # :nodoc:
-
2
def determine_constant_from_test_name(test_name)
-
5
names = test_name.split "::"
-
5
while names.size > 0 do
-
5
names.last.sub!(/Test$/, "")
-
5
begin
-
5
constant = names.join("::").safe_constantize
-
5
break(constant) if yield(constant)
-
ensure
-
5
names.pop
-
end
-
end
-
end
-
end
-
-
end
-
end
-
end
-
2
module ActiveSupport
-
2
module Testing
-
2
module Declarative
-
2
unless defined?(Spec)
-
# Helper to define a test method using a String. Under the hood, it replaces
-
# spaces with underscores and defines the test method.
-
#
-
# test "verify something" do
-
# ...
-
# end
-
2
def test(name, &block)
-
39
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
-
39
defined = method_defined? test_name
-
39
raise "#{test_name} is already defined in #{self}" if defined
-
39
if block_given?
-
39
define_method(test_name, &block)
-
else
-
define_method(test_name) do
-
flunk "No implementation provided for #{name}"
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/deprecation'
-
-
2
module ActiveSupport
-
2
module Testing
-
2
module Deprecation #:nodoc:
-
2
def assert_deprecated(match = nil, &block)
-
result, warnings = collect_deprecations(&block)
-
assert !warnings.empty?, "Expected a deprecation warning within the block but received none"
-
if match
-
match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp)
-
assert warnings.any? { |w| w =~ match }, "No deprecation warning matched #{match}: #{warnings.join(', ')}"
-
end
-
result
-
end
-
-
2
def assert_not_deprecated(&block)
-
result, deprecations = collect_deprecations(&block)
-
assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}"
-
result
-
end
-
-
2
def collect_deprecations
-
old_behavior = ActiveSupport::Deprecation.behavior
-
deprecations = []
-
ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
-
deprecations << message
-
end
-
result = yield
-
[result, deprecations]
-
ensure
-
ActiveSupport::Deprecation.behavior = old_behavior
-
end
-
end
-
end
-
end
-
2
require 'rbconfig'
-
-
2
module ActiveSupport
-
2
module Testing
-
2
module Isolation
-
2
require 'thread'
-
-
2
def self.included(klass) #:nodoc:
-
klass.class_eval do
-
parallelize_me!
-
end
-
end
-
-
2
def self.forking_env?
-
2
!ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/))
-
end
-
-
2
@@class_setup_mutex = Mutex.new
-
-
2
def _run_class_setup # class setup method should only happen in parent
-
@@class_setup_mutex.synchronize do
-
unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST']
-
self.class.setup if self.class.respond_to?(:setup)
-
@@ran_class_setup = true
-
end
-
end
-
end
-
-
2
def run
-
serialized = run_in_isolation do
-
super
-
end
-
-
Marshal.load(serialized)
-
end
-
-
2
module Forking
-
2
def run_in_isolation(&blk)
-
read, write = IO.pipe
-
read.binmode
-
write.binmode
-
-
pid = fork do
-
read.close
-
yield
-
write.puts [Marshal.dump(self.dup)].pack("m")
-
exit!
-
end
-
-
write.close
-
result = read.read
-
Process.wait2(pid)
-
return result.unpack("m")[0]
-
end
-
end
-
-
2
module Subprocess
-
2
ORIG_ARGV = ARGV.dup unless defined?(ORIG_ARGV)
-
-
# Crazy H4X to get this working in windows / jruby with
-
# no forking.
-
2
def run_in_isolation(&blk)
-
require "tempfile"
-
-
if ENV["ISOLATION_TEST"]
-
yield
-
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
-
file.puts [Marshal.dump(self.dup)].pack("m")
-
end
-
exit!
-
else
-
Tempfile.open("isolation") do |tmpfile|
-
env = {
-
ISOLATION_TEST: self.class.name,
-
ISOLATION_OUTPUT: tmpfile.path
-
}
-
-
load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
-
orig_args = ORIG_ARGV.join(" ")
-
test_opts = "-n#{self.class.name}##{self.name}"
-
command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}"
-
-
# IO.popen lets us pass env in a cross-platform way
-
child = IO.popen([env, command])
-
-
begin
-
Process.wait(child.pid)
-
rescue Errno::ECHILD # The child process may exit before we wait
-
nil
-
end
-
-
return tmpfile.read.unpack("m")[0]
-
end
-
end
-
end
-
end
-
-
2
include forking_env? ? Forking : Subprocess
-
end
-
end
-
end
-
2
require 'active_support/concern'
-
2
require 'active_support/callbacks'
-
-
2
module ActiveSupport
-
2
module Testing
-
# Adds support for +setup+ and +teardown+ callbacks.
-
# These callbacks serve as a replacement to overwriting the
-
# <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
-
#
-
# class ExampleTest < ActiveSupport::TestCase
-
# setup do
-
# # ...
-
# end
-
#
-
# teardown do
-
# # ...
-
# end
-
# end
-
2
module SetupAndTeardown
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
include ActiveSupport::Callbacks
-
2
define_callbacks :setup, :teardown
-
end
-
-
2
module ClassMethods
-
# Add a callback, which runs before <tt>TestCase#setup</tt>.
-
2
def setup(*args, &block)
-
19
set_callback(:setup, :before, *args, &block)
-
end
-
-
# Add a callback, which runs after <tt>TestCase#teardown</tt>.
-
2
def teardown(*args, &block)
-
4
set_callback(:teardown, :after, *args, &block)
-
end
-
end
-
-
2
def before_setup # :nodoc:
-
39
super
-
39
run_callbacks :setup
-
end
-
-
2
def after_teardown # :nodoc:
-
39
run_callbacks :teardown
-
39
super
-
end
-
end
-
end
-
end
-
2
module ActiveSupport
-
2
module Testing
-
# Logs a "PostsControllerTest: test name" heading before each test to
-
# make test.log easier to search and follow along with.
-
2
module TaggedLogging #:nodoc:
-
2
attr_writer :tagged_logger
-
-
2
def before_setup
-
39
if tagged_logger && tagged_logger.info?
-
39
heading = "#{self.class}: #{name}"
-
39
divider = '-' * heading.size
-
39
tagged_logger.info divider
-
39
tagged_logger.info heading
-
39
tagged_logger.info divider
-
end
-
39
super
-
end
-
-
2
private
-
2
def tagged_logger
-
195
@tagged_logger ||= (defined?(Rails.logger) && Rails.logger)
-
end
-
end
-
end
-
end
-
2
module ActiveSupport
-
2
module Testing
-
2
class SimpleStubs # :nodoc:
-
2
Stub = Struct.new(:object, :method_name, :original_method)
-
-
2
def initialize
-
@stubs = {}
-
end
-
-
2
def stub_object(object, method_name, return_value)
-
key = [object.object_id, method_name]
-
-
if stub = @stubs[key]
-
unstub_object(stub)
-
end
-
-
new_name = "__simple_stub__#{method_name}"
-
-
@stubs[key] = Stub.new(object, method_name, new_name)
-
-
object.singleton_class.send :alias_method, new_name, method_name
-
object.define_singleton_method(method_name) { return_value }
-
end
-
-
2
def unstub_all!
-
@stubs.each_value do |stub|
-
unstub_object(stub)
-
end
-
@stubs = {}
-
end
-
-
2
private
-
-
2
def unstub_object(stub)
-
singleton_class = stub.object.singleton_class
-
singleton_class.send :undef_method, stub.method_name
-
singleton_class.send :alias_method, stub.method_name, stub.original_method
-
singleton_class.send :undef_method, stub.original_method
-
end
-
end
-
-
# Containing helpers that helps you test passage of time.
-
2
module TimeHelpers
-
# Changes current time to the time in the future or in the past by a given time difference by
-
# stubbing +Time.now+ and +Date.today+.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel 1.day
-
# Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
-
# Date.current # => Sun, 10 Nov 2013
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel 1.day do
-
# User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
-
# end
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
2
def travel(duration, &block)
-
travel_to Time.now + duration, &block
-
end
-
-
# Changes current time to the given time by stubbing +Time.now+ and
-
# +Date.today+ to return the time or date passed into this method.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.new(2004, 11, 24, 01, 04, 44)
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# Date.current # => Wed, 24 Nov 2004
-
#
-
# Dates are taken as their timestamp at the beginning of the day in the
-
# application time zone. <tt>Time.current</tt> returns said timestamp,
-
# and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
-
# <tt>Date.current</tt> returns a date equal to the argument, and
-
# <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
-
# be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
-
# or <tt>Date.today</tt>, in order to honor the application time zone
-
# please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
-
#
-
# Note that the usec for the time passed will be set to 0 to prevent rounding
-
# errors with external services, like MySQL (which will round instead of floor,
-
# leading to off-by-one-second errors).
-
#
-
# This method also accepts a block, which will return the current time back to its original
-
# state at the end of the block:
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.new(2004, 11, 24, 01, 04, 44) do
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# end
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
2
def travel_to(date_or_time)
-
if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
-
now = date_or_time.midnight.to_time
-
else
-
now = date_or_time.to_time.change(usec: 0)
-
end
-
-
simple_stubs.stub_object(Time, :now, now)
-
simple_stubs.stub_object(Date, :today, now.to_date)
-
-
if block_given?
-
begin
-
yield
-
ensure
-
travel_back
-
end
-
end
-
end
-
-
# Returns the current time back to its original state, by removing the stubs added by
-
# `travel` and `travel_to`.
-
#
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
# travel_to Time.new(2004, 11, 24, 01, 04, 44)
-
# Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
-
# travel_back
-
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
-
2
def travel_back
-
simple_stubs.unstub_all!
-
end
-
-
2
private
-
-
2
def simple_stubs
-
@simple_stubs ||= SimpleStubs.new
-
end
-
end
-
end
-
end
-
2
module Arel
-
2
module Visitors
-
2
module BindVisitor
-
2
def initialize target
-
@block = nil
-
super
-
end
-
-
2
def accept node, collector, &block
-
@block = block if block_given?
-
super
-
end
-
-
2
private
-
-
2
def visit_Arel_Nodes_Assignment o, collector
-
if o.right.is_a? Arel::Nodes::BindParam
-
collector = visit o.left, collector
-
collector << " = "
-
visit o.right, collector
-
else
-
super
-
end
-
end
-
-
2
def visit_Arel_Nodes_BindParam o, collector
-
if @block
-
val = @block.call
-
if String === val
-
collector << val
-
end
-
else
-
super
-
end
-
end
-
-
end
-
end
-
end
-
2
require 'pathname'
-
-
2
module AutoprefixerRails
-
# Register autoprefixer postprocessor in Sprockets and fix common issues
-
2
class Sprockets
-
2
def self.register_processor(processor)
-
2
@processor = processor
-
end
-
-
# Sprockets 3 and 4 API
-
2
def self.call(input)
-
filename = input[:filename]
-
source = input[:data]
-
run(filename, source)
-
end
-
-
# Sprockets 2 compatibility
-
2
def self.process(context, css)
-
self.run(context.pathname.to_s, css)
-
end
-
-
# Add prefixes to `css`
-
2
def self.run(filename, css)
-
output = filename.chomp(File.extname(filename)) + '.css'
-
result = @processor.process(css, from: filename, to: output)
-
-
result.warnings.each do |warning|
-
$stderr.puts "autoprefixer: #{ warning }"
-
end
-
-
result.css
-
end
-
-
# Register postprocessor in Sprockets depend on issues with other gems
-
2
def self.install(env)
-
2
if ::Sprockets::VERSION.to_i < 4
-
2
env.register_postprocessor('text/css', :autoprefixer) do |context, css|
-
process(context, css)
-
end
-
else
-
env.register_bundle_processor('text/css',
-
::AutoprefixerRails::Sprockets)
-
end
-
end
-
-
# Register postprocessor in Sprockets depend on issues with other gems
-
2
def self.uninstall(env)
-
if ::Sprockets::VERSION.to_i < 4
-
env.unregister_postprocessor('text/css', :autoprefixer)
-
else
-
env.unregister_bundle_processor('text/css',
-
::AutoprefixerRails::Sprockets)
-
end
-
end
-
-
# Sprockets 2 API new and render
-
2
def initialize(filename, &block)
-
@filename = filename
-
@source = block.call
-
end
-
-
# Sprockets 2 API new and render
-
2
def render(_, _)
-
self.class.run(@filename, @source)
-
end
-
end
-
end
-
# All Devise controllers are inherited from here.
-
1
class DeviseController < Devise.parent_controller.constantize
-
1
include Devise::Controllers::ScopedViews
-
-
1
if respond_to?(:helper)
-
1
helper DeviseHelper
-
end
-
-
1
if respond_to?(:helper_method)
-
1
helpers = %w(resource scope_name resource_name signed_in_resource
-
resource_class resource_params devise_mapping)
-
1
helper_method(*helpers)
-
end
-
-
1
prepend_before_action :assert_is_devise_resource!
-
1
respond_to :html if mimes_for_respond_to.empty?
-
-
# Override prefixes to consider the scoped view.
-
# Notice we need to check for the request due to a bug in
-
# Action Controller tests that forces _prefixes to be
-
# loaded before even having a request object.
-
#
-
# This method should be public as it is is in ActionPack
-
# itself. Changing its visibility may break other gems.
-
1
def _prefixes #:nodoc:
-
@_prefixes ||= if self.class.scoped_views? && request && devise_mapping
-
["#{devise_mapping.scoped_path}/#{controller_name}"] + super
-
else
-
super
-
end
-
end
-
-
1
protected
-
-
# Gets the actual resource stored in the instance variable
-
1
def resource
-
instance_variable_get(:"@#{resource_name}")
-
end
-
-
# Proxy to devise map name
-
1
def resource_name
-
devise_mapping.name
-
end
-
1
alias :scope_name :resource_name
-
-
# Proxy to devise map class
-
1
def resource_class
-
devise_mapping.to
-
end
-
-
# Returns a signed in resource from session (if one exists)
-
1
def signed_in_resource
-
warden.authenticate(scope: resource_name)
-
end
-
-
# Attempt to find the mapped route for devise based on request path
-
1
def devise_mapping
-
@devise_mapping ||= request.env["devise.mapping"]
-
end
-
-
# Checks whether it's a devise mapped resource or not.
-
1
def assert_is_devise_resource! #:nodoc:
-
unknown_action! <<-MESSAGE unless devise_mapping
-
Could not find devise mapping for path #{request.fullpath.inspect}.
-
This may happen for two reasons:
-
-
1) You forgot to wrap your route inside the scope block. For example:
-
-
devise_scope :user do
-
get "/some/route" => "some_devise_controller"
-
end
-
-
2) You are testing a Devise controller bypassing the router.
-
If so, you can explicitly tell Devise which mapping to use:
-
-
@request.env["devise.mapping"] = Devise.mappings[:user]
-
-
MESSAGE
-
end
-
-
# Returns real navigational formats which are supported by Rails
-
1
def navigational_formats
-
@navigational_formats ||= Devise.navigational_formats.select { |format| Mime::EXTENSION_LOOKUP[format.to_s] }
-
end
-
-
1
def unknown_action!(msg)
-
logger.debug "[Devise] #{msg}" if logger
-
raise AbstractController::ActionNotFound, msg
-
end
-
-
# Sets the resource creating an instance variable
-
1
def resource=(new_resource)
-
instance_variable_set(:"@#{resource_name}", new_resource)
-
end
-
-
# Helper for use in before_actions where no authentication is required.
-
#
-
# Example:
-
# before_action :require_no_authentication, only: :new
-
1
def require_no_authentication
-
assert_is_devise_resource!
-
return unless is_navigational_format?
-
no_input = devise_mapping.no_input_strategies
-
-
authenticated = if no_input.present?
-
args = no_input.dup.push scope: resource_name
-
warden.authenticate?(*args)
-
else
-
warden.authenticated?(resource_name)
-
end
-
-
if authenticated && resource = warden.user(resource_name)
-
flash[:alert] = I18n.t("devise.failure.already_authenticated")
-
redirect_to after_sign_in_path_for(resource)
-
end
-
end
-
-
# Helper for use after calling send_*_instructions methods on a resource.
-
# If we are in paranoid mode, we always act as if the resource was valid
-
# and instructions were sent.
-
1
def successfully_sent?(resource)
-
notice = if Devise.paranoid
-
resource.errors.clear
-
:send_paranoid_instructions
-
elsif resource.errors.empty?
-
:send_instructions
-
end
-
-
if notice
-
set_flash_message! :notice, notice
-
true
-
end
-
end
-
-
# Sets the flash message with :key, using I18n. By default you are able
-
# to set up your messages using specific resource scope, and if no message is
-
# found we look to the default scope. Set the "now" options key to a true
-
# value to populate the flash.now hash in lieu of the default flash hash (so
-
# the flash message will be available to the current action instead of the
-
# next action).
-
# Example (i18n locale file):
-
#
-
# en:
-
# devise:
-
# passwords:
-
# #default_scope_messages - only if resource_scope is not found
-
# user:
-
# #resource_scope_messages
-
#
-
# Please refer to README or en.yml locale file to check what messages are
-
# available.
-
1
def set_flash_message(key, kind, options = {})
-
message = find_message(kind, options)
-
if options[:now]
-
flash.now[key] = message if message.present?
-
else
-
flash[key] = message if message.present?
-
end
-
end
-
-
# Sets flash message if is_flashing_format? equals true
-
1
def set_flash_message!(key, kind, options = {})
-
if is_flashing_format?
-
set_flash_message(key, kind, options)
-
end
-
end
-
-
# Sets minimum password length to show to user
-
1
def set_minimum_password_length
-
if devise_mapping.validatable?
-
@minimum_password_length = resource_class.password_length.min
-
end
-
end
-
-
1
def devise_i18n_options(options)
-
options
-
end
-
-
# Get message for given
-
1
def find_message(kind, options = {})
-
options[:scope] ||= translation_scope
-
options[:default] = Array(options[:default]).unshift(kind.to_sym)
-
options[:resource_name] = resource_name
-
options = devise_i18n_options(options)
-
I18n.t("#{options[:resource_name]}.#{kind}", options)
-
end
-
-
# Controllers inheriting DeviseController are advised to override this
-
# method so that other controllers inheriting from them would use
-
# existing translations.
-
1
def translation_scope
-
"devise.#{controller_name}"
-
end
-
-
1
def clean_up_passwords(object)
-
object.clean_up_passwords if object.respond_to?(:clean_up_passwords)
-
end
-
-
1
def respond_with_navigational(*args, &block)
-
respond_with(*args) do |format|
-
format.any(*navigational_formats, &block)
-
end
-
end
-
-
1
def resource_params
-
params.fetch(resource_name, {})
-
end
-
-
1
ActiveSupport.run_load_hooks(:devise_controller, self)
-
end
-
2
module DeviseHelper
-
# A simple way to show error messages for the current devise resource. If you need
-
# to customize this method, you can either overwrite it in your application helpers or
-
# copy the views to your application.
-
#
-
# This method is intended to stay simple and it is unlikely that we are going to change
-
# it to add more behavior or options.
-
2
def devise_error_messages!
-
return "" if resource.errors.empty?
-
-
messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
-
sentence = I18n.t("errors.messages.not_saved",
-
count: resource.errors.count,
-
resource: resource.class.model_name.human.downcase)
-
-
html = <<-HTML
-
<div id="error_explanation">
-
<h2>#{sentence}</h2>
-
<ul>#{messages}</ul>
-
</div>
-
HTML
-
-
html.html_safe
-
end
-
end
-
1
module Devise
-
1
module Controllers
-
1
module ScopedViews
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
1
def scoped_views?
-
defined?(@scoped_views) ? @scoped_views : Devise.scoped_views
-
end
-
-
1
def scoped_views=(value)
-
@scoped_views = value
-
end
-
end
-
end
-
end
-
end
-
2
module Devise
-
2
module Controllers
-
# Create url helpers to be used with resource/scope configuration. Acts as
-
# proxies to the generated routes created by devise.
-
# Resource param can be a string or symbol, a class, or an instance object.
-
# Example using a :user resource:
-
#
-
# new_session_path(:user) => new_user_session_path
-
# session_path(:user) => user_session_path
-
# destroy_session_path(:user) => destroy_user_session_path
-
#
-
# new_password_path(:user) => new_user_password_path
-
# password_path(:user) => user_password_path
-
# edit_password_path(:user) => edit_user_password_path
-
#
-
# new_confirmation_path(:user) => new_user_confirmation_path
-
# confirmation_path(:user) => user_confirmation_path
-
#
-
# Those helpers are included by default to ActionController::Base.
-
#
-
# In case you want to add such helpers to another class, you can do
-
# that as long as this new class includes both url_helpers and
-
# mounted_helpers. Example:
-
#
-
# include Rails.application.routes.url_helpers
-
# include Rails.application.routes.mounted_helpers
-
#
-
2
module UrlHelpers
-
2
def self.remove_helpers!
-
2
self.instance_methods.map(&:to_s).grep(/_(url|path)$/).each do |method|
-
56
remove_method method
-
end
-
end
-
-
2
def self.generate_helpers!(routes=nil)
-
routes ||= begin
-
2
mappings = Devise.mappings.values.map(&:used_helpers).flatten.uniq
-
2
Devise::URL_HELPERS.slice(*mappings)
-
4
end
-
-
4
routes.each do |module_name, actions|
-
18
[:path, :url].each do |path_or_url|
-
36
actions.each do |action|
-
96
action = action ? "#{action}_" : ""
-
96
method = :"#{action}#{module_name}_#{path_or_url}"
-
-
96
define_method method do |resource_or_scope, *args|
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
-
router_name = Devise.mappings[scope].router_name
-
context = router_name ? send(router_name) : _devise_route_context
-
context.send("#{action}#{scope}_#{module_name}_#{path_or_url}", *args)
-
end
-
end
-
end
-
end
-
end
-
-
2
generate_helpers!(Devise::URL_HELPERS)
-
-
2
private
-
-
2
def _devise_route_context
-
@_devise_route_context ||= send(Devise.available_router_name)
-
end
-
end
-
end
-
end
-
2
module Devise
-
# Checks the scope in the given environment and returns the associated failure app.
-
2
class Delegator
-
2
def call(env)
-
failure_app(env).call(env)
-
end
-
-
2
def failure_app(env)
-
app = env["warden.options"] &&
-
(scope = env["warden.options"][:scope]) &&
-
Devise.mappings[scope.to_sym].failure_app
-
-
app || Devise::FailureApp
-
end
-
end
-
end
-
2
require 'bcrypt'
-
-
2
module Devise
-
2
module Encryptor
-
2
def self.digest(klass, password)
-
4
if klass.pepper.present?
-
password = "#{password}#{klass.pepper}"
-
end
-
4
::BCrypt::Password.create(password, cost: klass.stretches).to_s
-
end
-
-
2
def self.compare(klass, hashed_password, password)
-
return false if hashed_password.blank?
-
bcrypt = ::BCrypt::Password.new(hashed_password)
-
if klass.pepper.present?
-
password = "#{password}#{klass.pepper}"
-
end
-
password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
-
Devise.secure_compare(password, hashed_password)
-
end
-
end
-
end
-
2
require "action_controller/metal"
-
-
2
module Devise
-
# Failure application that will be called every time :warden is thrown from
-
# any strategy or hook. Responsible for redirect the user to the sign in
-
# page based on current scope and mapping. If no scope is given, redirect
-
# to the default_url.
-
2
class FailureApp < ActionController::Metal
-
2
include ActionController::UrlFor
-
2
include ActionController::Redirecting
-
-
2
include Rails.application.routes.url_helpers
-
2
include Rails.application.routes.mounted_helpers
-
-
2
include Devise::Controllers::StoreLocation
-
-
2
delegate :flash, to: :request
-
-
2
def self.call(env)
-
@respond ||= action(:respond)
-
@respond.call(env)
-
end
-
-
# Try retrieving the URL options from the parent controller (usually
-
# ApplicationController). Instance methods are not supported at the moment,
-
# so only the class-level attribute is used.
-
2
def self.default_url_options(*args)
-
if defined?(Devise.parent_controller.constantize)
-
Devise.parent_controller.constantize.try(:default_url_options) || {}
-
else
-
{}
-
end
-
end
-
-
2
def respond
-
if http_auth?
-
http_auth
-
elsif warden_options[:recall]
-
recall
-
else
-
redirect
-
end
-
end
-
-
2
def http_auth
-
self.status = 401
-
self.headers["WWW-Authenticate"] = %(Basic realm=#{Devise.http_authentication_realm.inspect}) if http_auth_header?
-
self.content_type = request.format.to_s
-
self.response_body = http_auth_body
-
end
-
-
2
def recall
-
config = Rails.application.config
-
-
header_info = if config.try(:relative_url_root)
-
base_path = Pathname.new(config.relative_url_root)
-
full_path = Pathname.new(attempted_path)
-
-
{ "SCRIPT_NAME" => config.relative_url_root,
-
"PATH_INFO" => '/' + full_path.relative_path_from(base_path).to_s }
-
else
-
{ "PATH_INFO" => attempted_path }
-
end
-
-
header_info.each do | var, value|
-
if request.respond_to?(:set_header)
-
request.set_header(var, value)
-
else
-
env[var] = value
-
end
-
end
-
-
flash.now[:alert] = i18n_message(:invalid) if is_flashing_format?
-
# self.response = recall_app(warden_options[:recall]).call(env)
-
self.response = recall_app(warden_options[:recall]).call(request.env)
-
end
-
-
2
def redirect
-
store_location!
-
if is_flashing_format?
-
if flash[:timedout] && flash[:alert]
-
flash.keep(:timedout)
-
flash.keep(:alert)
-
else
-
flash[:alert] = i18n_message
-
end
-
end
-
redirect_to redirect_url
-
end
-
-
2
protected
-
-
2
def i18n_options(options)
-
options
-
end
-
-
2
def i18n_message(default = nil)
-
message = warden_message || default || :unauthenticated
-
-
if message.is_a?(Symbol)
-
options = {}
-
options[:resource_name] = scope
-
options[:scope] = "devise.failure"
-
options[:default] = [message]
-
auth_keys = scope_class.authentication_keys
-
keys = (auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys).map { |key| scope_class.human_attribute_name(key) }
-
options[:authentication_keys] = keys.join(I18n.translate(:"support.array.words_connector"))
-
options = i18n_options(options)
-
-
I18n.t(:"#{scope}.#{message}", options)
-
else
-
message.to_s
-
end
-
end
-
-
2
def redirect_url
-
if warden_message == :timeout
-
flash[:timedout] = true if is_flashing_format?
-
-
path = if request.get?
-
attempted_path
-
else
-
request.referrer
-
end
-
-
path || scope_url
-
else
-
scope_url
-
end
-
end
-
-
2
def route(scope)
-
:"new_#{scope}_session_url"
-
end
-
-
2
def scope_url
-
opts = {}
-
route = route(scope)
-
opts[:format] = request_format unless skip_format?
-
-
config = Rails.application.config
-
-
if config.respond_to?(:relative_url_root)
-
# Rails 4.2 goes into an infinite loop if opts[:script_name] is unset
-
rails_4_2 = (Rails::VERSION::MAJOR >= 4) && (Rails::VERSION::MINOR >= 2)
-
if config.relative_url_root.present? || rails_4_2
-
opts[:script_name] = config.relative_url_root
-
end
-
end
-
-
router_name = Devise.mappings[scope].router_name || Devise.available_router_name
-
context = send(router_name)
-
-
if context.respond_to?(route)
-
context.send(route, opts)
-
elsif respond_to?(:root_url)
-
root_url(opts)
-
else
-
"/"
-
end
-
end
-
-
2
def skip_format?
-
%w(html */*).include? request_format.to_s
-
end
-
-
# Choose whether we should respond in a http authentication fashion,
-
# including 401 and optional headers.
-
#
-
# This method allows the user to explicitly disable http authentication
-
# on ajax requests in case they want to redirect on failures instead of
-
# handling the errors on their own. This is useful in case your ajax API
-
# is the same as your public API and uses a format like JSON (so you
-
# cannot mark JSON as a navigational format).
-
2
def http_auth?
-
if request.xhr?
-
Devise.http_authenticatable_on_xhr
-
else
-
!(request_format && is_navigational_format?)
-
end
-
end
-
-
# It does not make sense to send authenticate headers in ajax requests
-
# or if the user disabled them.
-
2
def http_auth_header?
-
scope_class.http_authenticatable && !request.xhr?
-
end
-
-
2
def http_auth_body
-
return i18n_message unless request_format
-
method = "to_#{request_format}"
-
if method == "to_xml"
-
{ error: i18n_message }.to_xml(root: "errors")
-
elsif {}.respond_to?(method)
-
{ error: i18n_message }.send(method)
-
else
-
i18n_message
-
end
-
end
-
-
2
def recall_app(app)
-
controller, action = app.split("#")
-
controller_name = ActiveSupport::Inflector.camelize(controller)
-
controller_klass = ActiveSupport::Inflector.constantize("#{controller_name}Controller")
-
controller_klass.action(action)
-
end
-
-
2
def warden
-
request.respond_to?(:get_header) ? request.get_header("warden") : env["warden"]
-
end
-
-
2
def warden_options
-
request.respond_to?(:get_header) ? request.get_header("warden.options") : env["warden.options"]
-
end
-
-
2
def warden_message
-
@message ||= warden.message || warden_options[:message]
-
end
-
-
2
def scope
-
@scope ||= warden_options[:scope] || Devise.default_scope
-
end
-
-
2
def scope_class
-
@scope_class ||= Devise.mappings[scope].to
-
end
-
-
2
def attempted_path
-
warden_options[:attempted_path]
-
end
-
-
# Stores requested uri to redirect the user after signing in. We cannot use
-
# scoped session provided by warden here, since the user is not authenticated
-
# yet, but we still need to store the uri based on scope, so different scopes
-
# would never use the same uri to redirect.
-
2
def store_location!
-
store_location_for(scope, attempted_path) if request.get? && !http_auth?
-
end
-
-
2
def is_navigational_format?
-
Devise.navigational_formats.include?(request_format)
-
end
-
-
# Check if flash messages should be emitted. Default is to do it on
-
# navigational formats
-
2
def is_flashing_format?
-
is_navigational_format?
-
end
-
-
2
def request_format
-
@request_format ||= request.format.try(:ref)
-
end
-
end
-
end
-
# Before logout hook to forget the user in the given scope, if it responds
-
# to forget_me! Also clear remember token to ensure the user won't be
-
# remembered again. Notice that we forget the user unless the record is not persisted.
-
# This avoids forgetting deleted users.
-
2
Warden::Manager.before_logout do |record, warden, options|
-
if record.respond_to?(:forget_me!)
-
Devise::Hooks::Proxy.new(warden).forget_me(record)
-
end
-
end
-
2
Warden::Manager.after_set_user except: :fetch do |record, warden, options|
-
scope = options[:scope]
-
if record.respond_to?(:remember_me) && options[:store] != false &&
-
record.remember_me && warden.authenticated?(scope)
-
Devise::Hooks::Proxy.new(warden).remember_me(record)
-
end
-
end
-
# After each sign in, update sign in time, sign in count and sign in IP.
-
# This is only triggered when the user is explicitly set (with set_user)
-
# and on authentication. Retrieving the user from session (:fetch) does
-
# not trigger it.
-
2
Warden::Manager.after_set_user except: :fetch do |record, warden, options|
-
if record.respond_to?(:update_tracked_fields!) && warden.authenticated?(options[:scope]) && !warden.request.env['devise.skip_trackable']
-
record.update_tracked_fields!(warden.request)
-
end
-
end
-
2
require 'devise/strategies/database_authenticatable'
-
-
2
module Devise
-
2
def self.bcrypt(klass, password)
-
ActiveSupport::Deprecation.warn "Devise.bcrypt is deprecated; use Devise::Encryptor.digest instead"
-
Devise::Encryptor.digest(klass, password)
-
end
-
-
2
module Models
-
# Authenticatable Module, responsible for hashing the password and
-
# validating the authenticity of a user while signing in.
-
#
-
# == Options
-
#
-
# DatabaseAuthenticatable adds the following options to devise_for:
-
#
-
# * +pepper+: a random string used to provide a more secure hash. Use
-
# `rake secret` to generate new keys.
-
#
-
# * +stretches+: the cost given to bcrypt.
-
#
-
# == Examples
-
#
-
# User.find(1).valid_password?('password123') # returns true/false
-
#
-
2
module DatabaseAuthenticatable
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
after_update :send_password_change_notification, if: :send_password_change_notification?
-
-
2
attr_reader :password, :current_password
-
2
attr_accessor :password_confirmation
-
end
-
-
2
def self.required_fields(klass)
-
[:encrypted_password] + klass.authentication_keys
-
end
-
-
# Generates a hashed password based on the given value.
-
# For legacy reasons, we use `encrypted_password` to store
-
# the hashed password.
-
2
def password=(new_password)
-
@password = new_password
-
self.encrypted_password = password_digest(@password) if @password.present?
-
end
-
-
# Verifies whether a password (ie from sign in) is the user password.
-
2
def valid_password?(password)
-
Devise::Encryptor.compare(self.class, encrypted_password, password)
-
end
-
-
# Set password and password confirmation to nil
-
2
def clean_up_passwords
-
self.password = self.password_confirmation = nil
-
end
-
-
# Update record attributes when :current_password matches, otherwise
-
# returns error on :current_password.
-
#
-
# This method also rejects the password field if it is blank (allowing
-
# users to change relevant information like the e-mail without changing
-
# their password). In case the password field is rejected, the confirmation
-
# is also rejected as long as it is also blank.
-
2
def update_with_password(params, *options)
-
current_password = params.delete(:current_password)
-
-
if params[:password].blank?
-
params.delete(:password)
-
params.delete(:password_confirmation) if params[:password_confirmation].blank?
-
end
-
-
result = if valid_password?(current_password)
-
update_attributes(params, *options)
-
else
-
self.assign_attributes(params, *options)
-
self.valid?
-
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
-
false
-
end
-
-
clean_up_passwords
-
result
-
end
-
-
# Updates record attributes without asking for the current password.
-
# Never allows a change to the current password. If you are using this
-
# method, you should probably override this method to protect other
-
# attributes you would not like to be updated without a password.
-
#
-
# Example:
-
#
-
# def update_without_password(params, *options)
-
# params.delete(:email)
-
# super(params)
-
# end
-
#
-
2
def update_without_password(params, *options)
-
params.delete(:password)
-
params.delete(:password_confirmation)
-
-
result = update_attributes(params, *options)
-
clean_up_passwords
-
result
-
end
-
-
# Destroy record when :current_password matches, otherwise returns
-
# error on :current_password. It also automatically rejects
-
# :current_password if it is blank.
-
2
def destroy_with_password(current_password)
-
result = if valid_password?(current_password)
-
destroy
-
else
-
self.valid?
-
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
-
false
-
end
-
-
result
-
end
-
-
# A callback initiated after successfully authenticating. This can be
-
# used to insert your own logic that is only run after the user successfully
-
# authenticates.
-
#
-
# Example:
-
#
-
# def after_database_authentication
-
# self.update_attribute(:invite_code, nil)
-
# end
-
#
-
2
def after_database_authentication
-
end
-
-
# A reliable way to expose the salt regardless of the implementation.
-
2
def authenticatable_salt
-
54
encrypted_password[0,29] if encrypted_password
-
end
-
-
2
def send_password_change_notification
-
send_devise_notification(:password_change)
-
end
-
-
2
protected
-
-
# Hashes the password using bcrypt. Custom hash functions should override
-
# this method to apply their own algorithm.
-
#
-
# See https://github.com/plataformatec/devise-encryptable for examples
-
# of other hashing engines.
-
2
def password_digest(password)
-
4
Devise::Encryptor.digest(self.class, password)
-
end
-
-
2
def send_password_change_notification?
-
self.class.send_password_change_notification && encrypted_password_changed?
-
end
-
-
2
module ClassMethods
-
2
Devise::Models.config(self, :pepper, :stretches, :send_password_change_notification)
-
-
# We assume this method already gets the sanitized values from the
-
# DatabaseAuthenticatable strategy. If you are using this method on
-
# your own, be sure to sanitize the conditions hash to only include
-
# the proper fields.
-
2
def find_for_database_authentication(conditions)
-
find_for_authentication(conditions)
-
end
-
end
-
end
-
end
-
end
-
2
module Devise
-
2
module Models
-
-
# Recoverable takes care of resetting the user password and send reset instructions.
-
#
-
# ==Options
-
#
-
# Recoverable adds the following options to devise_for:
-
#
-
# * +reset_password_keys+: the keys you want to use when recovering the password for an account
-
# * +reset_password_within+: the time period within which the password must be reset or the token expires.
-
# * +sign_in_after_reset_password+: whether or not to sign in the user automatically after a password reset.
-
#
-
# == Examples
-
#
-
# # resets the user password and save the record, true if valid passwords are given, otherwise false
-
# User.find(1).reset_password('password123', 'password123')
-
#
-
# # creates a new token and send it with instructions about how to reset the password
-
# User.find(1).send_reset_password_instructions
-
#
-
2
module Recoverable
-
2
extend ActiveSupport::Concern
-
-
2
def self.required_fields(klass)
-
[:reset_password_sent_at, :reset_password_token]
-
end
-
-
2
included do
-
2
before_update do
-
if (respond_to?(:email_changed?) && email_changed?) || encrypted_password_changed?
-
clear_reset_password_token
-
end
-
end
-
end
-
-
# Update password saving the record and clearing token. Returns true if
-
# the passwords are valid and the record was saved, false otherwise.
-
2
def reset_password(new_password, new_password_confirmation)
-
self.password = new_password
-
self.password_confirmation = new_password_confirmation
-
-
if respond_to?(:after_password_reset) && valid?
-
ActiveSupport::Deprecation.warn "after_password_reset is deprecated"
-
after_password_reset
-
end
-
-
save
-
end
-
-
2
def reset_password!(new_password, new_password_confirmation)
-
ActiveSupport::Deprecation.warn "reset_password! is deprecated in favor of reset_password"
-
reset_password(new_password, new_password_confirmation)
-
end
-
-
# Resets reset password token and send reset password instructions by email.
-
# Returns the token sent in the e-mail.
-
2
def send_reset_password_instructions
-
token = set_reset_password_token
-
send_reset_password_instructions_notification(token)
-
-
token
-
end
-
-
# Checks if the reset password token sent is within the limit time.
-
# We do this by calculating if the difference between today and the
-
# sending date does not exceed the confirm in time configured.
-
# Returns true if the resource is not responding to reset_password_sent_at at all.
-
# reset_password_within is a model configuration, must always be an integer value.
-
#
-
# Example:
-
#
-
# # reset_password_within = 1.day and reset_password_sent_at = today
-
# reset_password_period_valid? # returns true
-
#
-
# # reset_password_within = 5.days and reset_password_sent_at = 4.days.ago
-
# reset_password_period_valid? # returns true
-
#
-
# # reset_password_within = 5.days and reset_password_sent_at = 5.days.ago
-
# reset_password_period_valid? # returns false
-
#
-
# # reset_password_within = 0.days
-
# reset_password_period_valid? # will always return false
-
#
-
2
def reset_password_period_valid?
-
reset_password_sent_at && reset_password_sent_at.utc >= self.class.reset_password_within.ago.utc
-
end
-
-
2
protected
-
-
# Removes reset_password token
-
2
def clear_reset_password_token
-
self.reset_password_token = nil
-
self.reset_password_sent_at = nil
-
end
-
-
2
def set_reset_password_token
-
raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)
-
-
self.reset_password_token = enc
-
self.reset_password_sent_at = Time.now.utc
-
save(validate: false)
-
raw
-
end
-
-
2
def send_reset_password_instructions_notification(token)
-
send_devise_notification(:reset_password_instructions, token, {})
-
end
-
-
2
module ClassMethods
-
# Attempt to find a user by password reset token. If a user is found, return it
-
# If a user is not found, return nil
-
2
def with_reset_password_token(token)
-
reset_password_token = Devise.token_generator.digest(self, :reset_password_token, token)
-
to_adapter.find_first(reset_password_token: reset_password_token)
-
end
-
-
# Attempt to find a user by its email. If a record is found, send new
-
# password instructions to it. If user is not found, returns a new user
-
# with an email not found error.
-
# Attributes must contain the user's email
-
2
def send_reset_password_instructions(attributes={})
-
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
-
recoverable.send_reset_password_instructions if recoverable.persisted?
-
recoverable
-
end
-
-
# Attempt to find a user by its reset_password_token to reset its
-
# password. If a user is found and token is still valid, reset its password and automatically
-
# try saving the record. If not user is found, returns a new user
-
# containing an error in reset_password_token attribute.
-
# Attributes must contain reset_password_token, password and confirmation
-
2
def reset_password_by_token(attributes={})
-
original_token = attributes[:reset_password_token]
-
reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)
-
-
recoverable = find_or_initialize_with_error_by(:reset_password_token, reset_password_token)
-
-
if recoverable.persisted?
-
if recoverable.reset_password_period_valid?
-
recoverable.reset_password(attributes[:password], attributes[:password_confirmation])
-
else
-
recoverable.errors.add(:reset_password_token, :expired)
-
end
-
end
-
-
recoverable.reset_password_token = original_token if recoverable.reset_password_token.present?
-
recoverable
-
end
-
-
2
Devise::Models.config(self, :reset_password_keys, :reset_password_within, :sign_in_after_reset_password)
-
end
-
end
-
end
-
end
-
2
module Devise
-
2
module Models
-
# Registerable is responsible for everything related to registering a new
-
# resource (ie user sign up).
-
2
module Registerable
-
2
extend ActiveSupport::Concern
-
-
2
def self.required_fields(klass)
-
[]
-
end
-
-
2
module ClassMethods
-
# A convenience method that receives both parameters and session to
-
# initialize a user. This can be used by OAuth, for example, to send
-
# in the user token and be stored on initialization.
-
#
-
# By default discards all information sent by the session by calling
-
# new with params.
-
2
def new_with_session(params, session)
-
new(params)
-
end
-
end
-
end
-
end
-
end
-
2
require 'devise/strategies/rememberable'
-
2
require 'devise/hooks/rememberable'
-
2
require 'devise/hooks/forgetable'
-
-
2
module Devise
-
2
module Models
-
# Rememberable manages generating and clearing token for remember the user
-
# from a saved cookie. Rememberable also has utility methods for dealing
-
# with serializing the user into the cookie and back from the cookie, trying
-
# to lookup the record based on the saved information.
-
# You probably wouldn't use rememberable methods directly, they are used
-
# mostly internally for handling the remember token.
-
#
-
# == Options
-
#
-
# Rememberable adds the following options in devise_for:
-
#
-
# * +remember_for+: the time you want the user will be remembered without
-
# asking for credentials. After this time the user will be blocked and
-
# will have to enter their credentials again. This configuration is also
-
# used to calculate the expires time for the cookie created to remember
-
# the user. By default remember_for is 2.weeks.
-
#
-
# * +extend_remember_period+: if true, extends the user's remember period
-
# when remembered via cookie. False by default.
-
#
-
# * +rememberable_options+: configuration options passed to the created cookie.
-
#
-
# == Examples
-
#
-
# User.find(1).remember_me! # regenerating the token
-
# User.find(1).forget_me! # clearing the token
-
#
-
# # generating info to put into cookies
-
# User.serialize_into_cookie(user)
-
#
-
# # lookup the user based on the incoming cookie information
-
# User.serialize_from_cookie(cookie_string)
-
2
module Rememberable
-
2
extend ActiveSupport::Concern
-
-
2
attr_accessor :remember_me
-
-
2
def self.required_fields(klass)
-
[:remember_created_at]
-
end
-
-
2
def remember_me!
-
self.remember_token ||= self.class.remember_token if respond_to?(:remember_token)
-
self.remember_created_at ||= Time.now.utc
-
save(validate: false) if self.changed?
-
end
-
-
# If the record is persisted, remove the remember token (but only if
-
# it exists), and save the record without validations.
-
2
def forget_me!
-
return unless persisted?
-
self.remember_token = nil if respond_to?(:remember_token)
-
self.remember_created_at = nil if self.class.expire_all_remember_me_on_sign_out
-
save(validate: false)
-
end
-
-
2
def remember_expires_at
-
self.class.remember_for.from_now
-
end
-
-
2
def extend_remember_period
-
self.class.extend_remember_period
-
end
-
-
2
def rememberable_value
-
if respond_to?(:remember_token)
-
remember_token
-
elsif respond_to?(:authenticatable_salt) && (salt = authenticatable_salt.presence)
-
salt
-
else
-
raise "authenticable_salt returned nil for the #{self.class.name} model. " \
-
"In order to use rememberable, you must ensure a password is always set " \
-
"or have a remember_token column in your model or implement your own " \
-
"rememberable_value in the model with custom logic."
-
end
-
end
-
-
2
def rememberable_options
-
self.class.rememberable_options
-
end
-
-
# A callback initiated after successfully being remembered. This can be
-
# used to insert your own logic that is only run after the user is
-
# remembered.
-
#
-
# Example:
-
#
-
# def after_remembered
-
# self.update_attribute(:invite_code, nil)
-
# end
-
#
-
2
def after_remembered
-
end
-
-
2
def remember_me?(token, generated_at)
-
# TODO: Normalize the JSON type coercion along with the Timeoutable hook
-
# in a single place https://github.com/plataformatec/devise/blob/ffe9d6d406e79108cf32a2c6a1d0b3828849c40b/lib/devise/hooks/timeoutable.rb#L14-L18
-
if generated_at.is_a?(String)
-
generated_at = time_from_json(generated_at)
-
end
-
-
# The token is only valid if:
-
# 1. we have a date
-
# 2. the current time does not pass the expiry period
-
# 3. the record has a remember_created_at date
-
# 4. the token date is bigger than the remember_created_at
-
# 5. the token matches
-
generated_at.is_a?(Time) &&
-
(self.class.remember_for.ago < generated_at) &&
-
(generated_at > (remember_created_at || Time.now).utc) &&
-
Devise.secure_compare(rememberable_value, token)
-
end
-
-
2
private
-
-
2
def time_from_json(value)
-
if value =~ /\A\d+\.\d+\Z/
-
Time.at(value.to_f)
-
else
-
Time.parse(value) rescue nil
-
end
-
end
-
-
2
module ClassMethods
-
# Create the cookie key using the record id and remember_token
-
2
def serialize_into_cookie(record)
-
[record.to_key, record.rememberable_value, Time.now.utc.to_f.to_s]
-
end
-
-
# Recreate the user based on the stored cookie
-
2
def serialize_from_cookie(*args)
-
id, token, generated_at = *args
-
-
record = to_adapter.get(id)
-
record if record && record.remember_me?(token, generated_at)
-
end
-
-
# Generate a token checking if one does not already exist in the database.
-
2
def remember_token #:nodoc:
-
loop do
-
token = Devise.friendly_token
-
break token unless to_adapter.find_first({ remember_token: token })
-
end
-
end
-
-
2
Devise::Models.config(self, :remember_for, :extend_remember_period, :rememberable_options, :expire_all_remember_me_on_sign_out)
-
end
-
end
-
end
-
end
-
2
require 'devise/hooks/trackable'
-
-
2
module Devise
-
2
module Models
-
# Track information about your user sign in. It tracks the following columns:
-
#
-
# * sign_in_count - Increased every time a sign in is made (by form, openid, oauth)
-
# * current_sign_in_at - A timestamp updated when the user signs in
-
# * last_sign_in_at - Holds the timestamp of the previous sign in
-
# * current_sign_in_ip - The remote ip updated when the user sign in
-
# * last_sign_in_ip - Holds the remote ip of the previous sign in
-
#
-
2
module Trackable
-
2
def self.required_fields(klass)
-
[:current_sign_in_at, :current_sign_in_ip, :last_sign_in_at, :last_sign_in_ip, :sign_in_count]
-
end
-
-
2
def update_tracked_fields(request)
-
old_current, new_current = self.current_sign_in_at, Time.now.utc
-
self.last_sign_in_at = old_current || new_current
-
self.current_sign_in_at = new_current
-
-
old_current, new_current = self.current_sign_in_ip, request.remote_ip
-
self.last_sign_in_ip = old_current || new_current
-
self.current_sign_in_ip = new_current
-
-
self.sign_in_count ||= 0
-
self.sign_in_count += 1
-
end
-
-
2
def update_tracked_fields!(request)
-
update_tracked_fields(request)
-
save(validate: false)
-
end
-
end
-
end
-
end
-
2
module Devise
-
2
module Models
-
# Validatable creates all needed validations for a user email and password.
-
# It's optional, given you may want to create the validations by yourself.
-
# Automatically validate if the email is present, unique and its format is
-
# valid. Also tests presence of password, confirmation and length.
-
#
-
# == Options
-
#
-
# Validatable adds the following options to devise_for:
-
#
-
# * +email_regexp+: the regular expression used to validate e-mails;
-
# * +password_length+: a range expressing password length. Defaults to 8..72.
-
#
-
2
module Validatable
-
# All validations used by this module.
-
2
VALIDATIONS = [:validates_presence_of, :validates_uniqueness_of, :validates_format_of,
-
:validates_confirmation_of, :validates_length_of].freeze
-
-
2
def self.required_fields(klass)
-
[]
-
end
-
-
2
def self.included(base)
-
2
base.extend ClassMethods
-
2
assert_validations_api!(base)
-
-
2
base.class_eval do
-
2
validates_presence_of :email, if: :email_required?
-
2
validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
-
2
validates_format_of :email, with: email_regexp, allow_blank: true, if: :email_changed?
-
-
2
validates_presence_of :password, if: :password_required?
-
2
validates_confirmation_of :password, if: :password_required?
-
2
validates_length_of :password, within: password_length, allow_blank: true
-
end
-
end
-
-
2
def self.assert_validations_api!(base) #:nodoc:
-
12
unavailable_validations = VALIDATIONS.select { |v| !base.respond_to?(v) }
-
-
2
unless unavailable_validations.empty?
-
raise "Could not use :validatable module since #{base} does not respond " <<
-
"to the following methods: #{unavailable_validations.to_sentence}."
-
end
-
end
-
-
2
protected
-
-
# Checks whether a password is needed or not. For validations only.
-
# Passwords are always required if it's a new record, or if the password
-
# or confirmation are being set somewhere.
-
2
def password_required?
-
!persisted? || !password.nil? || !password_confirmation.nil?
-
end
-
-
2
def email_required?
-
true
-
end
-
-
2
module ClassMethods
-
2
Devise::Models.config(self, :email_regexp, :password_length)
-
end
-
end
-
end
-
end
-
2
require 'orm_adapter/adapters/active_record'
-
-
2
ActiveRecord::Base.extend Devise::Models
-
2
require 'devise/strategies/base'
-
-
2
module Devise
-
2
module Strategies
-
# This strategy should be used as basis for authentication strategies. It retrieves
-
# parameters both from params or from http authorization headers. See database_authenticatable
-
# for an example.
-
2
class Authenticatable < Base
-
2
attr_accessor :authentication_hash, :authentication_type, :password
-
-
2
def store?
-
super && !mapping.to.skip_session_storage.include?(authentication_type)
-
end
-
-
2
def valid?
-
valid_for_params_auth? || valid_for_http_auth?
-
end
-
-
# Override and set to false for things like OmniAuth that technically
-
# run through Authentication (user_set) very often, which would normally
-
# reset CSRF data in the session
-
2
def clean_up_csrf?
-
true
-
end
-
-
2
private
-
-
# Receives a resource and check if it is valid by calling valid_for_authentication?
-
# An optional block that will be triggered while validating can be optionally
-
# given as parameter. Check Devise::Models::Authenticatable.valid_for_authentication?
-
# for more information.
-
#
-
# In case the resource can't be validated, it will fail with the given
-
# unauthenticated_message.
-
2
def validate(resource, &block)
-
result = resource && resource.valid_for_authentication?(&block)
-
-
if result
-
true
-
else
-
if resource
-
fail!(resource.unauthenticated_message)
-
end
-
false
-
end
-
end
-
-
# Get values from params and set in the resource.
-
2
def remember_me(resource)
-
resource.remember_me = remember_me? if resource.respond_to?(:remember_me=)
-
end
-
-
# Should this resource be marked to be remembered?
-
2
def remember_me?
-
valid_params? && Devise::TRUE_VALUES.include?(params_auth_hash[:remember_me])
-
end
-
-
# Check if this is a valid strategy for http authentication by:
-
#
-
# * Validating if the model allows http authentication;
-
# * If any of the authorization headers were sent;
-
# * If all authentication keys are present;
-
#
-
2
def valid_for_http_auth?
-
http_authenticatable? && request.authorization && with_authentication_hash(:http_auth, http_auth_hash)
-
end
-
-
# Check if this is a valid strategy for params authentication by:
-
#
-
# * Validating if the model allows params authentication;
-
# * If the request hits the sessions controller through POST;
-
# * If the params[scope] returns a hash with credentials;
-
# * If all authentication keys are present;
-
#
-
2
def valid_for_params_auth?
-
params_authenticatable? && valid_params_request? &&
-
valid_params? && with_authentication_hash(:params_auth, params_auth_hash)
-
end
-
-
# Check if the model accepts this strategy as http authenticatable.
-
2
def http_authenticatable?
-
mapping.to.http_authenticatable?(authenticatable_name)
-
end
-
-
# Check if the model accepts this strategy as params authenticatable.
-
2
def params_authenticatable?
-
mapping.to.params_authenticatable?(authenticatable_name)
-
end
-
-
# Extract the appropriate subhash for authentication from params.
-
2
def params_auth_hash
-
params[scope]
-
end
-
-
# Extract a hash with attributes:values from the http params.
-
2
def http_auth_hash
-
keys = [http_authentication_key, :password]
-
Hash[*keys.zip(decode_credentials).flatten]
-
end
-
-
# By default, a request is valid if the controller set the proper env variable.
-
2
def valid_params_request?
-
!!env["devise.allow_params_authentication"]
-
end
-
-
# If the request is valid, finally check if params_auth_hash returns a hash.
-
2
def valid_params?
-
params_auth_hash.is_a?(Hash)
-
end
-
-
# Note: unlike `Model.valid_password?`, this method does not actually
-
# ensure that the password in the params matches the password stored in
-
# the database. It only checks if the password is *present*. Do not rely
-
# on this method for validating that a given password is correct.
-
2
def valid_password?
-
password.present?
-
end
-
-
# Helper to decode credentials from HTTP.
-
2
def decode_credentials
-
return [] unless request.authorization && request.authorization =~ /^Basic (.*)/mi
-
Base64.decode64($1).split(/:/, 2)
-
end
-
-
# Sets the authentication hash and the password from params_auth_hash or http_auth_hash.
-
2
def with_authentication_hash(auth_type, auth_values)
-
self.authentication_hash, self.authentication_type = {}, auth_type
-
self.password = auth_values[:password]
-
-
parse_authentication_key_values(auth_values, authentication_keys) &&
-
parse_authentication_key_values(request_values, request_keys)
-
end
-
-
2
def authentication_keys
-
@authentication_keys ||= mapping.to.authentication_keys
-
end
-
-
2
def http_authentication_key
-
@http_authentication_key ||= mapping.to.http_authentication_key || case authentication_keys
-
when Array then authentication_keys.first
-
when Hash then authentication_keys.keys.first
-
end
-
end
-
-
2
def request_keys
-
@request_keys ||= mapping.to.request_keys
-
end
-
-
2
def request_values
-
keys = request_keys.respond_to?(:keys) ? request_keys.keys : request_keys
-
values = keys.map { |k| self.request.send(k) }
-
Hash[keys.zip(values)]
-
end
-
-
2
def parse_authentication_key_values(hash, keys)
-
keys.each do |key, enforce|
-
value = hash[key].presence
-
if value
-
self.authentication_hash[key] = value
-
else
-
return false unless enforce == false
-
end
-
end
-
true
-
end
-
-
# Holds the authenticatable name for this class. Devise::Strategies::DatabaseAuthenticatable
-
# becomes simply :database.
-
2
def authenticatable_name
-
@authenticatable_name ||=
-
ActiveSupport::Inflector.underscore(self.class.name.split("::").last).
-
sub("_authenticatable", "").to_sym
-
end
-
end
-
end
-
end
-
2
module Devise
-
2
module Strategies
-
# Base strategy for Devise. Responsible for verifying correct scope and mapping.
-
2
class Base < ::Warden::Strategies::Base
-
# Whenever CSRF cannot be verified, we turn off any kind of storage
-
2
def store?
-
!env["devise.skip_storage"]
-
end
-
-
# Checks if a valid scope was given for devise and find mapping based on this scope.
-
2
def mapping
-
@mapping ||= begin
-
mapping = Devise.mappings[scope]
-
raise "Could not find mapping for #{scope}" unless mapping
-
mapping
-
end
-
end
-
end
-
end
-
end
-
2
require 'devise/strategies/authenticatable'
-
-
2
module Devise
-
2
module Strategies
-
# Default strategy for signing in a user, based on their email and password in the database.
-
2
class DatabaseAuthenticatable < Authenticatable
-
2
def authenticate!
-
resource = password.present? && mapping.to.find_for_database_authentication(authentication_hash)
-
hashed = false
-
-
if validate(resource){ hashed = true; resource.valid_password?(password) }
-
remember_me(resource)
-
resource.after_database_authentication
-
success!(resource)
-
end
-
-
mapping.to.new.password = password if !hashed && Devise.paranoid
-
fail(:not_found_in_database) unless resource
-
end
-
end
-
end
-
end
-
-
2
Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)
-
2
require 'devise/strategies/authenticatable'
-
-
2
module Devise
-
2
module Strategies
-
# Remember the user through the remember token. This strategy is responsible
-
# to verify whether there is a cookie with the remember token, and to
-
# recreate the user from this cookie if it exists. Must be called *before*
-
# authenticatable.
-
2
class Rememberable < Authenticatable
-
# A valid strategy for rememberable needs a remember token in the cookies.
-
2
def valid?
-
@remember_cookie = nil
-
remember_cookie.present?
-
end
-
-
# To authenticate a user we deserialize the cookie and attempt finding
-
# the record in the database. If the attempt fails, we pass to another
-
# strategy handle the authentication.
-
2
def authenticate!
-
resource = mapping.to.serialize_from_cookie(*remember_cookie)
-
-
unless resource
-
cookies.delete(remember_key)
-
return pass
-
end
-
-
if validate(resource)
-
remember_me(resource) if extend_remember_me?(resource)
-
resource.after_remembered
-
success!(resource)
-
end
-
end
-
-
# No need to clean up the CSRF when using rememberable.
-
# In fact, cleaning it up here would be a bug because
-
# rememberable is triggered on GET requests which means
-
# we would render a page on first access with all csrf
-
# tokens expired.
-
2
def clean_up_csrf?
-
false
-
end
-
-
2
private
-
-
2
def extend_remember_me?(resource)
-
resource.respond_to?(:extend_remember_period) && resource.extend_remember_period
-
end
-
-
2
def remember_me?
-
true
-
end
-
-
2
def remember_key
-
mapping.to.rememberable_options.fetch(:key, "remember_#{scope}_token")
-
end
-
-
2
def remember_cookie
-
@remember_cookie ||= cookies.signed[remember_key]
-
end
-
-
end
-
end
-
end
-
-
2
Warden::Strategies.add(:rememberable, Devise::Strategies::Rememberable)
-
1
module Devise
-
# Devise::TestHelpers provides a facility to test controllers in isolation
-
# when using ActionController::TestCase allowing you to quickly sign_in or
-
# sign_out a user. Do not use Devise::TestHelpers in integration tests.
-
#
-
# Notice you should not test Warden specific behavior (like Warden callbacks)
-
# using Devise::TestHelpers since it is a stub of the actual behavior. Such
-
# callbacks should be tested in your integration suite instead.
-
1
module TestHelpers
-
1
def self.included(base)
-
3
base.class_eval do
-
3
setup :setup_controller_for_warden, :warden if respond_to?(:setup)
-
end
-
end
-
-
# Override process to consider warden.
-
1
def process(*)
-
# Make sure we always return @response, a la ActionController::TestCase::Behaviour#process, even if warden interrupts
-
58
_catch_warden { super } # || @response # _catch_warden will setup the @response object
-
-
# process needs to return the ActionDispath::TestResponse object
-
29
@response
-
end
-
-
# We need to set up the environment variables and the response in the controller.
-
1
def setup_controller_for_warden #:nodoc:
-
27
@request.env['action_controller.instance'] = @controller
-
end
-
-
# Quick access to Warden::Proxy.
-
1
def warden #:nodoc:
-
81
@request.env['warden'] ||= begin
-
27
manager = Warden::Manager.new(nil) do |config|
-
27
config.merge! Devise.warden_config
-
end
-
27
Warden::Proxy.new(@request.env, manager)
-
end
-
end
-
-
# sign_in a given resource by storing its keys in the session.
-
# This method bypass any warden authentication callback.
-
#
-
# Examples:
-
#
-
# sign_in :user, @user # sign_in(scope, resource)
-
# sign_in @user # sign_in(resource)
-
#
-
1
def sign_in(resource_or_scope, resource=nil)
-
27
scope ||= Devise::Mapping.find_scope!(resource_or_scope)
-
27
resource ||= resource_or_scope
-
27
warden.instance_variable_get(:@users).delete(scope)
-
27
warden.session_serializer.store(resource, scope)
-
end
-
-
# Sign out a given resource or scope by calling logout on Warden.
-
# This method bypass any warden logout callback.
-
#
-
# Examples:
-
#
-
# sign_out :user # sign_out(scope)
-
# sign_out @user # sign_out(resource)
-
#
-
1
def sign_out(resource_or_scope)
-
scope = Devise::Mapping.find_scope!(resource_or_scope)
-
@controller.instance_variable_set(:"@current_#{scope}", nil)
-
user = warden.instance_variable_get(:@users).delete(scope)
-
warden.session_serializer.delete(scope, user)
-
end
-
-
1
protected
-
-
# Catch warden continuations and handle like the middleware would.
-
# Returns nil when interrupted, otherwise the normal result of the block.
-
1
def _catch_warden(&block)
-
29
result = catch(:warden, &block)
-
-
29
env = @controller.request.env
-
-
29
result ||= {}
-
-
# Set the response. In production, the rack result is returned
-
# from Warden::Manager#call, which the following is modelled on.
-
29
case result
-
when Array
-
if result.first == 401 && intercept_401?(env) # does this happen during testing?
-
_process_unauthenticated(env)
-
else
-
result
-
end
-
when Hash
-
_process_unauthenticated(env, result)
-
else
-
29
result
-
end
-
end
-
-
1
def _process_unauthenticated(env, options = {})
-
options[:action] ||= :unauthenticated
-
proxy = env['warden']
-
result = options[:result] || proxy.result
-
-
ret = case result
-
when :redirect
-
body = proxy.message || "You are being redirected to #{proxy.headers['Location']}"
-
[proxy.status, proxy.headers, [body]]
-
when :custom
-
proxy.custom_response
-
else
-
env["PATH_INFO"] = "/#{options[:action]}"
-
env["warden.options"] = options
-
Warden::Manager._run_callbacks(:before_failure, env, options)
-
-
status, headers, response = Devise.warden_config[:failure_app].call(env).to_a
-
@controller.response.headers.merge!(headers)
-
r_opts = { status: status, content_type: headers["Content-Type"], location: headers["Location"] }
-
r_opts[Rails.version.start_with?('5') ? :body : :text] = response.body
-
@controller.send :render, r_opts
-
nil # causes process return @response
-
end
-
-
# ensure that the controller response is set up. In production, this is
-
# not necessary since warden returns the results to rack. However, at
-
# testing time, we want the response to be available to the testing
-
# framework to verify what would be returned to rack.
-
if ret.is_a?(Array)
-
# ensure the controller response is set to our response.
-
@controller.response ||= @response
-
@response.status = ret.first
-
@response.headers.clear
-
ret.second.each { |k,v| @response[k] = v }
-
@response.body = ret.third
-
end
-
-
ret
-
end
-
end
-
end
-
2
require 'openssl'
-
-
2
module Devise
-
2
class TokenGenerator
-
2
def initialize(key_generator, digest = "SHA256")
-
2
@key_generator = key_generator
-
2
@digest = digest
-
end
-
-
2
def digest(klass, column, value)
-
value.present? && OpenSSL::HMAC.hexdigest(@digest, key_for(column), value.to_s)
-
end
-
-
2
def generate(klass, column)
-
key = key_for(column)
-
-
loop do
-
raw = Devise.friendly_token
-
enc = OpenSSL::HMAC.hexdigest(@digest, key, raw)
-
break [raw, enc] unless klass.to_adapter.find_first({ column => enc })
-
end
-
end
-
-
2
private
-
-
2
def key_for(column)
-
@key_generator.generate_key("Devise #{column}")
-
end
-
end
-
end
-
2
require 'active_support/concern'
-
-
2
class GlobalID
-
2
module Identification
-
2
extend ActiveSupport::Concern
-
-
2
def to_global_id(options = {})
-
@global_id ||= GlobalID.create(self, options)
-
end
-
2
alias to_gid to_global_id
-
-
2
def to_gid_param(options = {})
-
to_global_id(options).to_param
-
end
-
-
2
def to_signed_global_id(options = {})
-
SignedGlobalID.create(self, options)
-
end
-
2
alias to_sgid to_signed_global_id
-
-
2
def to_sgid_param(options = {})
-
to_signed_global_id(options).to_param
-
end
-
end
-
end
-
2
require 'global_id'
-
2
require 'active_support/message_verifier'
-
2
require 'time'
-
-
2
class SignedGlobalID < GlobalID
-
2
class ExpiredMessage < StandardError; end
-
-
2
class << self
-
2
attr_accessor :verifier
-
-
2
def parse(sgid, options = {})
-
if sgid.is_a? self
-
sgid
-
else
-
super verify(sgid, options), options
-
end
-
end
-
-
# Grab the verifier from options and fall back to SignedGlobalID.verifier.
-
# Raise ArgumentError if neither is available.
-
2
def pick_verifier(options)
-
options.fetch :verifier do
-
verifier || raise(ArgumentError, 'Pass a `verifier:` option with an `ActiveSupport::MessageVerifier` instance, or set a default SignedGlobalID.verifier.')
-
end
-
end
-
-
2
attr_accessor :expires_in
-
-
2
DEFAULT_PURPOSE = "default"
-
-
2
def pick_purpose(options)
-
options.fetch :for, DEFAULT_PURPOSE
-
end
-
-
2
private
-
2
def verify(sgid, options)
-
metadata = pick_verifier(options).verify(sgid)
-
-
raise_if_expired(metadata['expires_at'])
-
-
metadata['gid'] if pick_purpose(options) == metadata['purpose']
-
rescue ActiveSupport::MessageVerifier::InvalidSignature, ExpiredMessage
-
nil
-
end
-
-
2
def raise_if_expired(expires_at)
-
if expires_at && Time.now.utc > Time.iso8601(expires_at)
-
raise ExpiredMessage, 'This signed global id has expired.'
-
end
-
end
-
end
-
-
2
attr_reader :verifier, :purpose, :expires_at
-
-
2
def initialize(gid, options = {})
-
super
-
@verifier = self.class.pick_verifier(options)
-
@purpose = self.class.pick_purpose(options)
-
@expires_at = pick_expiration(options)
-
end
-
-
2
def to_s
-
@sgid ||= @verifier.generate(to_h)
-
end
-
2
alias to_param to_s
-
-
2
def to_h
-
# Some serializers decodes symbol keys to symbols, others to strings.
-
# Using string keys remedies that.
-
{ 'gid' => @uri.to_s, 'purpose' => purpose, 'expires_at' => encoded_expiration }
-
end
-
-
2
def ==(other)
-
super && @purpose == other.purpose
-
end
-
-
2
private
-
2
def encoded_expiration
-
expires_at.utc.iso8601(3) if expires_at
-
end
-
-
2
def pick_expiration(options)
-
return options[:expires_at] if options.key?(:expires_at)
-
-
if expires_in = options.fetch(:expires_in) { self.class.expires_in }
-
expires_in.from_now
-
end
-
end
-
end
-
2
module I18n
-
2
module Backend
-
2
autoload :Base, 'i18n/backend/base'
-
2
autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
-
2
autoload :Cache, 'i18n/backend/cache'
-
2
autoload :Cascade, 'i18n/backend/cascade'
-
2
autoload :Chain, 'i18n/backend/chain'
-
2
autoload :Fallbacks, 'i18n/backend/fallbacks'
-
2
autoload :Flatten, 'i18n/backend/flatten'
-
2
autoload :Gettext, 'i18n/backend/gettext'
-
2
autoload :KeyValue, 'i18n/backend/key_value'
-
2
autoload :Memoize, 'i18n/backend/memoize'
-
2
autoload :Metadata, 'i18n/backend/metadata'
-
2
autoload :Pluralization, 'i18n/backend/pluralization'
-
2
autoload :Simple, 'i18n/backend/simple'
-
2
autoload :Transliterator, 'i18n/backend/transliterator'
-
end
-
end
-
2
require 'yaml'
-
2
require 'i18n/core_ext/hash'
-
2
require 'i18n/core_ext/kernel/suppress_warnings'
-
-
2
module I18n
-
2
module Backend
-
2
module Base
-
2
include I18n::Backend::Transliterator
-
-
# Accepts a list of paths to translation files. Loads translations from
-
# plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
-
# for details.
-
2
def load_translations(*filenames)
-
1
filenames = I18n.load_path if filenames.empty?
-
9
filenames.flatten.each { |filename| load_file(filename) }
-
end
-
-
# This method receives a locale, a data hash and options for storing translations.
-
# Should be implemented
-
2
def store_translations(locale, data, options = {})
-
raise NotImplementedError
-
end
-
-
2
def translate(locale, key, options = {})
-
262
raise InvalidLocale.new(locale) unless locale
-
262
entry = key && lookup(locale, key, options[:scope], options)
-
-
262
if options.empty?
-
entry = resolve(locale, key, entry, options)
-
else
-
262
count, default = options.values_at(:count, :default)
-
262
values = options.except(*RESERVED_KEYS)
-
262
entry = entry.nil? && default ?
-
default(locale, key, default, options) : resolve(locale, key, entry, options)
-
end
-
-
262
throw(:exception, I18n::MissingTranslation.new(locale, key, options)) if entry.nil?
-
144
entry = entry.dup if entry.is_a?(String)
-
-
144
entry = pluralize(locale, entry, count) if count
-
144
entry = interpolate(locale, entry, values) if values
-
144
entry
-
end
-
-
2
def exists?(locale, key)
-
lookup(locale, key) != nil
-
end
-
-
# Acts the same as +strftime+, but uses a localized version of the
-
# format string. Takes a key from the date/time formats translations as
-
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
-
2
def localize(locale, object, format = :default, options = {})
-
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
-
-
if Symbol === format
-
key = format
-
type = object.respond_to?(:sec) ? 'time' : 'date'
-
options = options.merge(:raise => true, :object => object, :locale => locale)
-
format = I18n.t(:"#{type}.formats.#{key}", options)
-
end
-
-
# format = resolve(locale, object, format, options)
-
format = format.to_s.gsub(/%[aAbBpP]/) do |match|
-
case match
-
when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
-
when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
-
when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
-
when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
-
when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase if object.respond_to? :hour
-
when '%P' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase if object.respond_to? :hour
-
end
-
end
-
-
object.strftime(format)
-
end
-
-
# Returns an array of locales for which translations are available
-
# ignoring the reserved translation meta data key :i18n.
-
2
def available_locales
-
raise NotImplementedError
-
end
-
-
2
def reload!
-
end
-
-
2
protected
-
-
# The method which actually looks up for the translation in the store.
-
2
def lookup(locale, key, scope = [], options = {})
-
raise NotImplementedError
-
end
-
-
# Evaluates defaults.
-
# If given subject is an Array, it walks the array and returns the
-
# first translation that can be resolved. Otherwise it tries to resolve
-
# the translation directly.
-
2
def default(locale, object, subject, options = {})
-
444
options = options.dup.reject { |key, value| key == :default }
-
129
case subject
-
when Array
-
subject.each do |item|
-
247
result = resolve(locale, object, item, options) and return result
-
129
end and nil
-
else
-
resolve(locale, object, subject, options)
-
end
-
end
-
-
# Resolves a translation.
-
# If the given subject is a Symbol, it will be translated with the
-
# given options. If it is a Proc then it will be evaluated. All other
-
# subjects will be returned directly.
-
2
def resolve(locale, object, subject, options = {})
-
380
return subject if options[:resolve] == false
-
380
result = catch(:exception) do
-
380
case subject
-
when Symbol
-
125
I18n.translate(subject, options.merge(:locale => locale, :throw => true))
-
when Proc
-
date_or_time = options.delete(:object) || object
-
resolve(locale, object, subject.call(date_or_time, options))
-
else
-
255
subject
-
end
-
end
-
380
result unless result.is_a?(MissingTranslation)
-
end
-
-
# Picks a translation from a pluralized mnemonic subkey according to English
-
# pluralization rules :
-
# - It will pick the :one subkey if count is equal to 1.
-
# - It will pick the :other subkey otherwise.
-
# - It will pick the :zero subkey in the special case where count is
-
# equal to 0 and there is a :zero subkey present. This behaviour is
-
# not stand with regards to the CLDR pluralization rules.
-
# Other backends can implement more flexible or complex pluralization rules.
-
2
def pluralize(locale, entry, count)
-
85
return entry unless entry.is_a?(Hash) && count
-
-
key = :zero if count == 0 && entry.has_key?(:zero)
-
key ||= count == 1 ? :one : :other
-
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
-
entry[key]
-
end
-
-
# Interpolates values into a given string.
-
#
-
# interpolate "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
-
# # => "file test.txt opened by %{user}"
-
2
def interpolate(locale, string, values = {})
-
144
if string.is_a?(::String) && !values.empty?
-
111
I18n.interpolate(string, values)
-
else
-
33
string
-
end
-
end
-
-
# Loads a single translations file by delegating to #load_rb or
-
# #load_yml depending on the file extension and directly merges the
-
# data to the existing translations. Raises I18n::UnknownFileType
-
# for all other file extensions.
-
2
def load_file(filename)
-
8
type = File.extname(filename).tr('.', '').downcase
-
8
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
-
8
data = send(:"load_#{type}", filename)
-
8
unless data.is_a?(Hash)
-
raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
-
end
-
16
data.each { |locale, d| store_translations(locale, d || {}) }
-
end
-
-
# Loads a plain Ruby translations file. eval'ing the file must yield
-
# a Hash containing translation data with locales as toplevel keys.
-
2
def load_rb(filename)
-
eval(IO.read(filename), binding, filename)
-
end
-
-
# Loads a YAML translations file. The data must have locales as
-
# toplevel keys.
-
2
def load_yml(filename)
-
8
begin
-
8
YAML.load_file(filename)
-
rescue TypeError, ScriptError, StandardError => e
-
raise InvalidLocaleData.new(filename, e.inspect)
-
end
-
end
-
end
-
end
-
end
-
2
module I18n
-
2
module Backend
-
# A simple backend that reads translations from YAML files and stores them in
-
# an in-memory hash. Relies on the Base backend.
-
#
-
# The implementation is provided by a Implementation module allowing to easily
-
# extend Simple backend's behavior by including modules. E.g.:
-
#
-
# module I18n::Backend::Pluralization
-
# def pluralize(*args)
-
# # extended pluralization logic
-
# super
-
# end
-
# end
-
#
-
# I18n::Backend::Simple.include(I18n::Backend::Pluralization)
-
2
class Simple
-
6
(class << self; self; end).class_eval { public :include }
-
-
2
module Implementation
-
2
include Base
-
-
2
def initialized?
-
263
@initialized ||= false
-
end
-
-
# Stores translations for the given locale in memory.
-
# This uses a deep merge for the translations hash, so existing
-
# translations will be overwritten by new ones only at the deepest
-
# level of the hash.
-
2
def store_translations(locale, data, options = {})
-
8
locale = locale.to_sym
-
8
translations[locale] ||= {}
-
8
data = data.deep_symbolize_keys
-
8
translations[locale].deep_merge!(data)
-
end
-
-
# Get available locales from the translations hash
-
2
def available_locales
-
1
init_translations unless initialized?
-
1
translations.inject([]) do |locales, (locale, data)|
-
1
locales << locale unless (data.keys - [:i18n]).empty?
-
1
locales
-
end
-
end
-
-
# Clean up translations hash and set initialized to false on reload!
-
2
def reload!
-
2
@initialized = false
-
2
@translations = nil
-
2
super
-
end
-
-
2
protected
-
-
2
def init_translations
-
1
load_translations
-
1
@initialized = true
-
end
-
-
2
def translations
-
279
@translations ||= {}
-
end
-
-
# Looks up a translation from the translations hash. Returns nil if
-
# eiher key is nil, or locale, scope or key do not exist as a key in the
-
# nested translations hash. Splits keys or scopes containing dots
-
# into multiple keys, i.e. <tt>currency.format</tt> is regarded the same as
-
# <tt>%w(currency format)</tt>.
-
2
def lookup(locale, key, scope = [], options = {})
-
262
init_translations unless initialized?
-
262
keys = I18n.normalize_keys(locale, key, scope, options[:separator])
-
-
262
keys.inject(translations) do |result, _key|
-
781
_key = _key.to_sym
-
781
return nil unless result.is_a?(Hash) && result.has_key?(_key)
-
534
result = result[_key]
-
534
result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
-
534
result
-
end
-
end
-
end
-
-
2
include Implementation
-
end
-
end
-
end
-
# encoding: utf-8
-
2
module I18n
-
2
module Backend
-
2
module Transliterator
-
2
DEFAULT_REPLACEMENT_CHAR = "?"
-
-
# Given a locale and a UTF-8 string, return the locale's ASCII
-
# approximation for the string.
-
2
def transliterate(locale, string, replacement = nil)
-
@transliterators ||= {}
-
@transliterators[locale] ||= Transliterator.get I18n.t(:'i18n.transliterate.rule',
-
:locale => locale, :resolve => false, :default => {})
-
@transliterators[locale].transliterate(string, replacement)
-
end
-
-
# Get a transliterator instance.
-
2
def self.get(rule = nil)
-
if !rule || rule.kind_of?(Hash)
-
HashTransliterator.new(rule)
-
elsif rule.kind_of? Proc
-
ProcTransliterator.new(rule)
-
else
-
raise I18n::ArgumentError, "Transliteration rule must be a proc or a hash."
-
end
-
end
-
-
# A transliterator which accepts a Proc as its transliteration rule.
-
2
class ProcTransliterator
-
2
def initialize(rule)
-
@rule = rule
-
end
-
-
2
def transliterate(string, replacement = nil)
-
@rule.call(string)
-
end
-
end
-
-
# A transliterator which accepts a Hash of characters as its translation
-
# rule.
-
2
class HashTransliterator
-
2
DEFAULT_APPROXIMATIONS = {
-
"À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE",
-
"Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
-
"Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
-
"Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
-
"Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a",
-
"ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e",
-
"ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d",
-
"ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o",
-
"ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y",
-
"Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C",
-
"ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c",
-
"Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E",
-
"ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e",
-
"Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G",
-
"ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i",
-
"Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I",
-
"ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k",
-
"ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l",
-
"Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N",
-
"ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng",
-
"Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE",
-
"œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r",
-
"Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S",
-
"š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t",
-
"Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U",
-
"ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w",
-
"Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z",
-
"Ž"=>"Z", "ž"=>"z"
-
}.freeze
-
-
2
def initialize(rule = nil)
-
@rule = rule
-
add DEFAULT_APPROXIMATIONS.dup
-
add rule if rule
-
end
-
-
2
def transliterate(string, replacement = nil)
-
string.gsub(/[^\x00-\x7f]/u) do |char|
-
approximations[char] || replacement || DEFAULT_REPLACEMENT_CHAR
-
end
-
end
-
-
2
private
-
-
2
def approximations
-
@approximations ||= {}
-
end
-
-
# Add transliteration rules to the approximations hash.
-
2
def add(hash)
-
hash.each do |key, value|
-
approximations[key.to_s] = value.to_s
-
end
-
end
-
end
-
end
-
end
-
end
-
2
class Hash
-
def slice(*keep_keys)
-
h = {}
-
keep_keys.each { |key| h[key] = fetch(key) }
-
h
-
2
end unless Hash.method_defined?(:slice)
-
-
def except(*less_keys)
-
slice(*keys - less_keys)
-
2
end unless Hash.method_defined?(:except)
-
-
def deep_symbolize_keys
-
inject({}) { |result, (key, value)|
-
value = value.deep_symbolize_keys if value.is_a?(Hash)
-
result[(key.to_sym rescue key) || key] = value
-
result
-
}
-
2
end unless Hash.method_defined?(:deep_symbolize_keys)
-
-
# deep_merge_hash! by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
-
2
MERGER = proc do |key, v1, v2|
-
Hash === v1 && Hash === v2 ? v1.merge(v2, &MERGER) : v2
-
end
-
-
def deep_merge!(data)
-
merge!(data, &MERGER)
-
2
end unless Hash.method_defined?(:deep_merge!)
-
end
-
-
2
module Kernel
-
2
def suppress_warnings
-
original_verbosity, $VERBOSE = $VERBOSE, nil
-
yield
-
ensure
-
$VERBOSE = original_verbosity
-
end
-
end
-
2
require 'jbuilder/jbuilder'
-
-
2
dependency_tracker = false
-
-
2
begin
-
2
require 'action_view'
-
2
require 'action_view/dependency_tracker'
-
2
dependency_tracker = ::ActionView::DependencyTracker
-
rescue LoadError
-
begin
-
require 'cache_digests'
-
dependency_tracker = ::CacheDigests::DependencyTracker
-
rescue LoadError
-
end
-
end
-
-
2
if dependency_tracker
-
2
class Jbuilder
-
2
module DependencyTrackerMethods
-
# Matches:
-
# json.partial! "messages/message"
-
# json.partial!('messages/message')
-
#
-
2
DIRECT_RENDERS = /
-
\w+\.partial! # json.partial!
-
\(?\s* # optional parenthesis
-
(['"])([^'"]+)\1 # quoted value
-
/x
-
-
# Matches:
-
# json.partial! partial: "comments/comment"
-
# json.comments @post.comments, partial: "comments/comment", as: :comment
-
# json.array! @posts, partial: "posts/post", as: :post
-
# = render partial: "account"
-
#
-
2
INDIRECT_RENDERS = /
-
(?::partial\s*=>|partial:) # partial: or :partial =>
-
\s* # optional whitespace
-
(['"])([^'"]+)\1 # quoted value
-
/x
-
-
2
def dependencies
-
direct_dependencies + indirect_dependencies + explicit_dependencies
-
end
-
-
2
private
-
-
2
def direct_dependencies
-
source.scan(DIRECT_RENDERS).map(&:second)
-
end
-
-
2
def indirect_dependencies
-
source.scan(INDIRECT_RENDERS).map(&:second)
-
end
-
end
-
end
-
-
2
::Jbuilder::DependencyTracker = Class.new(dependency_tracker::ERBTracker)
-
2
::Jbuilder::DependencyTracker.send :include, ::Jbuilder::DependencyTrackerMethods
-
2
dependency_tracker.register_tracker :jbuilder, ::Jbuilder::DependencyTracker
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail # :doc:
-
-
2
require 'date'
-
2
require 'shellwords'
-
-
2
require 'uri'
-
2
require 'net/smtp'
-
-
2
begin
-
# Use mime/types/columnar if available, for reduced memory usage
-
2
require 'mime/types/columnar'
-
rescue LoadError
-
require 'mime/types'
-
end
-
-
2
if RUBY_VERSION <= '1.8.6'
-
begin
-
require 'tlsmail'
-
rescue LoadError
-
raise "You need to install tlsmail if you are using ruby <= 1.8.6"
-
end
-
end
-
-
2
if RUBY_VERSION >= "1.9.0"
-
2
require 'mail/version_specific/ruby_1_9'
-
2
RubyVer = Ruby19
-
else
-
require 'mail/version_specific/ruby_1_8'
-
RubyVer = Ruby18
-
end
-
-
2
require 'mail/version'
-
-
2
require 'mail/core_extensions/string'
-
2
require 'mail/core_extensions/smtp' if RUBY_VERSION < '1.9.3'
-
2
require 'mail/indifferent_hash'
-
-
# Only load our multibyte extensions if AS is not already loaded
-
2
if defined?(ActiveSupport)
-
2
require 'active_support/inflector'
-
else
-
require 'mail/core_extensions/string/access'
-
require 'mail/core_extensions/string/multibyte'
-
require 'mail/multibyte'
-
end
-
-
2
require 'mail/constants'
-
2
require 'mail/utilities'
-
2
require 'mail/configuration'
-
-
2
@@autoloads = {}
-
2
def self.register_autoload(name, path)
-
108
@@autoloads[name] = path
-
108
autoload(name, path)
-
end
-
-
# This runs through the autoload list and explictly requires them for you.
-
# Useful when running mail in a threaded process.
-
#
-
# Usage:
-
#
-
# require 'mail'
-
# Mail.eager_autoload!
-
2
def self.eager_autoload!
-
@@autoloads.each { |_,path| require(path) }
-
end
-
-
# Autoload mail send and receive classes.
-
2
require 'mail/network'
-
-
2
require 'mail/message'
-
2
require 'mail/part'
-
2
require 'mail/header'
-
2
require 'mail/parts_list'
-
2
require 'mail/attachments_list'
-
2
require 'mail/body'
-
2
require 'mail/field'
-
2
require 'mail/field_list'
-
-
2
require 'mail/envelope'
-
-
2
register_autoload :Parsers, "mail/parsers"
-
-
# Autoload header field elements and transfer encodings.
-
2
require 'mail/elements'
-
2
require 'mail/encodings'
-
2
require 'mail/encodings/base64'
-
2
require 'mail/encodings/quoted_printable'
-
2
require 'mail/encodings/unix_to_unix'
-
-
2
require 'mail/matchers/has_sent_mail'
-
2
require 'mail/matchers/attachment_matchers.rb'
-
-
# Finally... require all the Mail.methods
-
2
require 'mail/mail'
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
class AttachmentsList < Array
-
-
2
def initialize(parts_list)
-
@parts_list = parts_list
-
@content_disposition_type = 'attachment'
-
parts_list.map { |p|
-
if p.content_type == "message/rfc822"
-
Mail.new(p.body).attachments
-
elsif p.parts.empty?
-
p if p.attachment?
-
else
-
p.attachments
-
end
-
}.flatten.compact.each { |a| self << a }
-
self
-
end
-
-
2
def inline
-
@content_disposition_type = 'inline'
-
self
-
end
-
-
# Returns the attachment by filename or at index.
-
#
-
# mail.attachments['test.png'] = File.read('test.png')
-
# mail.attachments['test.jpg'] = File.read('test.jpg')
-
#
-
# mail.attachments['test.png'].filename #=> 'test.png'
-
# mail.attachments[1].filename #=> 'test.jpg'
-
2
def [](index_value)
-
if index_value.is_a?(Fixnum)
-
self.fetch(index_value)
-
else
-
self.select { |a| a.filename == index_value }.first
-
end
-
end
-
-
2
def []=(name, value)
-
encoded_name = Mail::Encodings.decode_encode name, :encode
-
default_values = { :content_type => "#{set_mime_type(name)}; filename=\"#{encoded_name}\"",
-
:content_transfer_encoding => "#{guess_encoding}",
-
:content_disposition => "#{@content_disposition_type}; filename=\"#{encoded_name}\"" }
-
-
if value.is_a?(Hash)
-
-
default_values[:body] = value.delete(:content) if value[:content]
-
-
default_values[:body] = value.delete(:data) if value[:data]
-
-
encoding = value.delete(:transfer_encoding) || value.delete(:encoding)
-
if encoding
-
if Mail::Encodings.defined? encoding
-
default_values[:content_transfer_encoding] = encoding
-
else
-
raise "Do not know how to handle Content Transfer Encoding #{encoding}, please choose either quoted-printable or base64"
-
end
-
end
-
-
if value[:mime_type]
-
default_values[:content_type] = value.delete(:mime_type)
-
@mime_type = MIME::Types[default_values[:content_type]].first
-
default_values[:content_transfer_encoding] ||= guess_encoding
-
end
-
-
hash = default_values.merge(value)
-
else
-
default_values[:body] = value
-
hash = default_values
-
end
-
-
if hash[:body].respond_to? :force_encoding and hash[:body].respond_to? :valid_encoding?
-
if not hash[:body].valid_encoding? and default_values[:content_transfer_encoding].downcase == "binary"
-
hash[:body] = hash[:body].dup if hash[:body].frozen?
-
hash[:body].force_encoding("BINARY")
-
end
-
end
-
-
attachment = Part.new(hash)
-
attachment.add_content_id(hash[:content_id])
-
-
@parts_list << attachment
-
end
-
-
# Uses the mime type to try and guess the encoding, if it is a binary type, or unknown, then we
-
# set it to binary, otherwise as set to plain text
-
2
def guess_encoding
-
if @mime_type && !@mime_type.binary?
-
"7bit"
-
else
-
"binary"
-
end
-
end
-
-
2
def set_mime_type(filename)
-
# Have to do this because MIME::Types is not Ruby 1.9 safe yet
-
if RUBY_VERSION >= '1.9'
-
filename = filename.encode(Encoding::UTF_8) if filename.respond_to?(:encode)
-
end
-
-
@mime_type = MIME::Types.type_for(filename).first
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
-
# = Body
-
#
-
# The body is where the text of the email is stored. Mail treats the body
-
# as a single object. The body itself has no information about boundaries
-
# used in the MIME standard, it just looks at its content as either a single
-
# block of text, or (if it is a multipart message) as an array of blocks of text.
-
#
-
# A body has to be told to split itself up into a multipart message by calling
-
# #split with the correct boundary. This is because the body object has no way
-
# of knowing what the correct boundary is for itself (there could be many
-
# boundaries in a body in the case of a nested MIME text).
-
#
-
# Once split is called, Mail::Body will slice itself up on this boundary,
-
# assigning anything that appears before the first part to the preamble, and
-
# anything that appears after the closing boundary to the epilogue, then
-
# each part gets initialized into a Mail::Part object.
-
#
-
# The boundary that is used to split up the Body is also stored in the Body
-
# object for use on encoding itself back out to a string. You can
-
# overwrite this if it needs to be changed.
-
#
-
# On encoding, the body will return the preamble, then each part joined by
-
# the boundary, followed by a closing boundary string and then the epilogue.
-
2
class Body
-
-
2
def initialize(string = '')
-
@boundary = nil
-
@preamble = nil
-
@epilogue = nil
-
@charset = nil
-
@part_sort_order = [ "text/plain", "text/enriched", "text/html" ]
-
@parts = Mail::PartsList.new
-
if Utilities.blank?(string)
-
@raw_source = ''
-
else
-
# Do join first incase we have been given an Array in Ruby 1.9
-
if string.respond_to?(:join)
-
@raw_source = string.join('')
-
elsif string.respond_to?(:to_s)
-
@raw_source = string.to_s
-
else
-
raise "You can only assign a string or an object that responds_to? :join or :to_s to a body."
-
end
-
end
-
@encoding = (only_us_ascii? ? '7bit' : '8bit')
-
set_charset
-
end
-
-
# Matches this body with another body. Also matches the decoded value of this
-
# body with a string.
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body == body #=> true
-
#
-
# body = Mail::Body.new('The body')
-
# body == 'The body' #=> true
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body == "The body" #=> true
-
2
def ==(other)
-
if other.class == String
-
self.decoded == other
-
else
-
super
-
end
-
end
-
-
# Accepts a string and performs a regular expression against the decoded text
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body =~ /The/ #=> 0
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body =~ /The/ #=> 0
-
2
def =~(regexp)
-
self.decoded =~ regexp
-
end
-
-
# Accepts a string and performs a regular expression against the decoded text
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body.match(/The/) #=> #<MatchData "The">
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body.match(/The/) #=> #<MatchData "The">
-
2
def match(regexp)
-
self.decoded.match(regexp)
-
end
-
-
# Accepts anything that responds to #to_s and checks if it's a substring of the decoded text
-
#
-
# Examples:
-
#
-
# body = Mail::Body.new('The body')
-
# body.include?('The') #=> true
-
#
-
# body = Mail::Body.new("VGhlIGJvZHk=\n")
-
# body.encoding = 'base64'
-
# body.include?('The') #=> true
-
2
def include?(other)
-
self.decoded.include?(other.to_s)
-
end
-
-
# Allows you to set the sort order of the parts, overriding the default sort order.
-
# Defaults to 'text/plain', then 'text/enriched', then 'text/html' with any other content
-
# type coming after.
-
2
def set_sort_order(order)
-
@part_sort_order = order
-
end
-
-
# Allows you to sort the parts according to the default sort order, or the sort order you
-
# set with :set_sort_order.
-
#
-
# sort_parts! is also called from :encode, so there is no need for you to call this explicitly
-
2
def sort_parts!
-
@parts.each do |p|
-
p.body.set_sort_order(@part_sort_order)
-
p.body.sort_parts!
-
end
-
@parts.sort!(@part_sort_order)
-
end
-
-
# Returns the raw source that the body was initialized with, without
-
# any tampering
-
2
def raw_source
-
@raw_source
-
end
-
-
2
def get_best_encoding(target)
-
target_encoding = Mail::Encodings.get_encoding(target)
-
target_encoding.get_best_compatible(encoding, raw_source)
-
end
-
-
# Returns a body encoded using transfer_encoding. Multipart always uses an
-
# identiy encoding (i.e. no encoding).
-
# Calling this directly is not a good idea, but supported for compatibility
-
# TODO: Validate that preamble and epilogue are valid for requested encoding
-
2
def encoded(transfer_encoding = '8bit')
-
if multipart?
-
self.sort_parts!
-
encoded_parts = parts.map { |p| p.encoded }
-
([preamble] + encoded_parts).join(crlf_boundary) + end_boundary + epilogue.to_s
-
else
-
be = get_best_encoding(transfer_encoding)
-
dec = Mail::Encodings::get_encoding(encoding)
-
enc = Mail::Encodings::get_encoding(be)
-
if dec.nil?
-
# Cannot decode, so skip normalization
-
raw_source
-
else
-
# Decode then encode to normalize and allow transforming
-
# from base64 to Q-P and vice versa
-
decoded = dec.decode(raw_source)
-
if defined?(Encoding) && charset && charset != "US-ASCII"
-
decoded.encode!(charset)
-
decoded.force_encoding('BINARY') unless Encoding.find(charset).ascii_compatible?
-
end
-
enc.encode(decoded)
-
end
-
end
-
end
-
-
2
def decoded
-
if !Encodings.defined?(encoding)
-
raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
-
else
-
Encodings.get_encoding(encoding).decode(raw_source)
-
end
-
end
-
-
2
def to_s
-
decoded
-
end
-
-
2
def charset
-
@charset
-
end
-
-
2
def charset=( val )
-
@charset = val
-
end
-
-
2
def encoding(val = nil)
-
if val
-
self.encoding = val
-
else
-
@encoding
-
end
-
end
-
-
2
def encoding=( val )
-
@encoding = if val == "text" || Utilities.blank?(val)
-
(only_us_ascii? ? '7bit' : '8bit')
-
else
-
val
-
end
-
end
-
-
# Returns the preamble (any text that is before the first MIME boundary)
-
2
def preamble
-
@preamble
-
end
-
-
# Sets the preamble to a string (adds text before the first MIME boundary)
-
2
def preamble=( val )
-
@preamble = val
-
end
-
-
# Returns the epilogue (any text that is after the last MIME boundary)
-
2
def epilogue
-
@epilogue
-
end
-
-
# Sets the epilogue to a string (adds text after the last MIME boundary)
-
2
def epilogue=( val )
-
@epilogue = val
-
end
-
-
# Returns true if there are parts defined in the body
-
2
def multipart?
-
true unless parts.empty?
-
end
-
-
# Returns the boundary used by the body
-
2
def boundary
-
@boundary
-
end
-
-
# Allows you to change the boundary of this Body object
-
2
def boundary=( val )
-
@boundary = val
-
end
-
-
2
def parts
-
@parts
-
end
-
-
2
def <<( val )
-
if @parts
-
@parts << val
-
else
-
@parts = Mail::PartsList.new[val]
-
end
-
end
-
-
2
def split!(boundary)
-
self.boundary = boundary
-
parts = extract_parts
-
-
# Make the preamble equal to the preamble (if any)
-
self.preamble = parts[0].to_s.strip
-
# Make the epilogue equal to the epilogue (if any)
-
self.epilogue = parts[-1].to_s.strip
-
parts[1...-1].to_a.each { |part| @parts << Mail::Part.new(part) }
-
self
-
end
-
-
2
def only_us_ascii?
-
!(raw_source =~ /[^\x01-\x7f]/)
-
end
-
-
2
def empty?
-
!!raw_source.to_s.empty?
-
end
-
-
2
private
-
-
# split parts by boundary, ignore first part if empty, append final part when closing boundary was missing
-
2
def extract_parts
-
parts_regex = /
-
(?: # non-capturing group
-
\A | # start of string OR
-
\r\n # line break
-
)
-
(
-
--#{Regexp.escape(boundary || "")} # boundary delimiter
-
(?:--)? # with non-capturing optional closing
-
)
-
(?=\s*$) # lookahead matching zero or more spaces followed by line-ending
-
/x
-
parts = raw_source.split(parts_regex).each_slice(2).to_a
-
parts.each_with_index { |(part, _), index| parts.delete_at(index) if index > 0 && Utilities.blank?(part) }
-
-
if parts.size > 1
-
final_separator = parts[-2][1]
-
parts << [""] if final_separator != "--#{boundary}--"
-
end
-
parts.map(&:first)
-
end
-
-
2
def crlf_boundary
-
"\r\n--#{boundary}\r\n"
-
end
-
-
2
def end_boundary
-
"\r\n--#{boundary}--\r\n"
-
end
-
-
2
def set_charset
-
only_us_ascii? ? @charset = 'US-ASCII' : @charset = nil
-
end
-
end
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
module CheckDeliveryParams
-
2
def check_delivery_params(mail)
-
if Utilities.blank?(mail.smtp_envelope_from)
-
raise ArgumentError.new('An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address.')
-
end
-
-
if Utilities.blank?(mail.smtp_envelope_to)
-
raise ArgumentError.new('An SMTP To address is required to send a message. Set the message smtp_envelope_to, to, cc, or bcc address.')
-
end
-
-
message = mail.encoded if mail.respond_to?(:encoded)
-
if Utilities.blank?(message)
-
raise ArgumentError.new('An encoded message is required to send an email')
-
end
-
-
[mail.smtp_envelope_from, mail.smtp_envelope_to, message]
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# Thanks to Nicolas Fouché for this wrapper
-
#
-
2
require 'singleton'
-
-
2
module Mail
-
-
# The Configuration class is a Singleton used to hold the default
-
# configuration for all Mail objects.
-
#
-
# Each new mail object gets a copy of these values at initialization
-
# which can be overwritten on a per mail object basis.
-
2
class Configuration
-
2
include Singleton
-
-
2
def initialize
-
@delivery_method = nil
-
@retriever_method = nil
-
super
-
end
-
-
2
def delivery_method(method = nil, settings = {})
-
return @delivery_method if @delivery_method && method.nil?
-
@delivery_method = lookup_delivery_method(method).new(settings)
-
end
-
-
2
def lookup_delivery_method(method)
-
case method.is_a?(String) ? method.to_sym : method
-
when nil
-
Mail::SMTP
-
when :smtp
-
Mail::SMTP
-
when :sendmail
-
Mail::Sendmail
-
when :exim
-
Mail::Exim
-
when :file
-
Mail::FileDelivery
-
when :smtp_connection
-
Mail::SMTPConnection
-
when :test
-
Mail::TestMailer
-
else
-
method
-
end
-
end
-
-
2
def retriever_method(method = nil, settings = {})
-
return @retriever_method if @retriever_method && method.nil?
-
@retriever_method = lookup_retriever_method(method).new(settings)
-
end
-
-
2
def lookup_retriever_method(method)
-
case method
-
when nil
-
Mail::POP3
-
when :pop3
-
Mail::POP3
-
when :imap
-
Mail::IMAP
-
when :test
-
Mail::TestRetriever
-
else
-
method
-
end
-
end
-
-
2
def param_encode_language(value = nil)
-
value ? @encode_language = value : @encode_language ||= 'en'
-
end
-
-
end
-
-
end
-
# encoding: us-ascii
-
# frozen_string_literal: true
-
2
module Mail
-
2
module Constants
-
2
white_space = %Q|\x9\x20|
-
2
text = %Q|\x1-\x8\xB\xC\xE-\x7f|
-
2
field_name = %Q|\x21-\x39\x3b-\x7e|
-
2
qp_safe = %Q|\x20-\x3c\x3e-\x7e|
-
-
2
aspecial = %Q|()<>[]:;@\\,."| # RFC5322
-
2
tspecial = %Q|()<>@,;:\\"/[]?=| # RFC2045
-
2
sp = %Q| |
-
2
control = %Q|\x00-\x1f\x7f-\xff|
-
-
2
if control.respond_to?(:force_encoding)
-
2
control = control.dup.force_encoding(Encoding::BINARY)
-
end
-
-
2
CRLF = /\r\n/
-
2
WSP = /[#{white_space}]/
-
2
FWS = /#{CRLF}#{WSP}*/
-
2
TEXT = /[#{text}]/ # + obs-text
-
2
FIELD_NAME = /[#{field_name}]+/
-
2
FIELD_PREFIX = /\A(#{FIELD_NAME})/
-
2
FIELD_BODY = /.+/m
-
2
FIELD_LINE = /^[#{field_name}]+:\s*.+$/
-
2
FIELD_SPLIT = /^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/
-
2
HEADER_LINE = /^([#{field_name}]+:\s*.+)$/
-
2
HEADER_SPLIT = /#{CRLF}(?!#{WSP})/
-
-
2
QP_UNSAFE = /[^#{qp_safe}]/
-
2
QP_SAFE = /[#{qp_safe}]/
-
2
CONTROL_CHAR = /[#{control}]/n
-
2
ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{sp}]/n
-
2
PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
-
2
TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{sp}]/n
-
2
ENCODED_VALUE = /\=\?([^?]+)\?([QB])\?[^?]*?\?\=/mi
-
2
FULL_ENCODED_VALUE = /(\=\?[^?]+\?[QB]\?[^?]*?\?\=)/mi
-
-
2
EMPTY = ''
-
2
SPACE = ' '
-
2
UNDERSCORE = '_'
-
2
HYPHEN = '-'
-
2
COLON = ':'
-
2
ASTERISK = '*'
-
2
CR = "\r"
-
2
LF = "\n"
-
2
CR_ENCODED = "=0D"
-
2
LF_ENCODED = "=0A"
-
2
CAPITAL_M = 'M'
-
2
EQUAL_LF = "=\n"
-
2
NULL_SENDER = '<>'
-
-
2
Q_VALUES = ['Q','q']
-
2
B_VALUES = ['B','b']
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
class String #:nodoc:
-
-
2
unless method_defined?(:ascii_only?)
-
# Backport from Ruby 1.9 checks for non-us-ascii characters.
-
def ascii_only?
-
self !~ MATCH_NON_US_ASCII
-
end
-
-
MATCH_NON_US_ASCII = /[^\x00-\x7f]/
-
end
-
-
2
def not_ascii_only?
-
!ascii_only?
-
end
-
-
2
unless method_defined?(:bytesize)
-
alias :bytesize :length
-
end
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
register_autoload :Address, 'mail/elements/address'
-
2
register_autoload :AddressList, 'mail/elements/address_list'
-
2
register_autoload :ContentDispositionElement, 'mail/elements/content_disposition_element'
-
2
register_autoload :ContentLocationElement, 'mail/elements/content_location_element'
-
2
register_autoload :ContentTransferEncodingElement, 'mail/elements/content_transfer_encoding_element'
-
2
register_autoload :ContentTypeElement, 'mail/elements/content_type_element'
-
2
register_autoload :DateTimeElement, 'mail/elements/date_time_element'
-
2
register_autoload :EnvelopeFromElement, 'mail/elements/envelope_from_element'
-
2
register_autoload :MessageIdsElement, 'mail/elements/message_ids_element'
-
2
register_autoload :MimeVersionElement, 'mail/elements/mime_version_element'
-
2
register_autoload :PhraseList, 'mail/elements/phrase_list'
-
2
register_autoload :ReceivedElement, 'mail/elements/received_element'
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
2
module Mail
-
# Raised when attempting to decode an unknown encoding type
-
2
class UnknownEncodingType < StandardError #:nodoc:
-
end
-
-
2
module Encodings
-
-
2
include Mail::Constants
-
2
extend Mail::Utilities
-
-
2
@transfer_encodings = {}
-
-
# Register transfer encoding
-
#
-
# Example
-
#
-
# Encodings.register "base64", Mail::Encodings::Base64
-
2
def Encodings.register(name, cls)
-
12
@transfer_encodings[get_name(name)] = cls
-
end
-
-
# Is the encoding we want defined?
-
#
-
# Example:
-
#
-
# Encodings.defined?(:base64) #=> true
-
2
def Encodings.defined?( str )
-
@transfer_encodings.include? get_name(str)
-
end
-
-
# Gets a defined encoding type, QuotedPrintable or Base64 for now.
-
#
-
# Each encoding needs to be defined as a Mail::Encodings::ClassName for
-
# this to work, allows us to add other encodings in the future.
-
#
-
# Example:
-
#
-
# Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
-
2
def Encodings.get_encoding( str )
-
@transfer_encodings[get_name(str)]
-
end
-
-
2
def Encodings.get_all
-
@transfer_encodings.values
-
end
-
-
2
def Encodings.get_name(enc)
-
12
underscoreize(enc).downcase
-
end
-
-
2
def Encodings.transcode_charset(str, from_charset, to_charset = 'UTF-8')
-
if from_charset
-
RubyVer.transcode_charset str, from_charset, to_charset
-
else
-
str
-
end
-
end
-
-
# Encodes a parameter value using URI Escaping, note the language field 'en' can
-
# be set using Mail::Configuration, like so:
-
#
-
# Mail.defaults do
-
# param_encode_language 'jp'
-
# end
-
#
-
# The character set used for encoding will either be the value of $KCODE for
-
# Ruby < 1.9 or the encoding on the string passed in.
-
#
-
# Example:
-
#
-
# Mail::Encodings.param_encode("This is fun") #=> "us-ascii'en'This%20is%20fun"
-
2
def Encodings.param_encode(str)
-
case
-
when str.ascii_only? && str =~ TOKEN_UNSAFE
-
%Q{"#{str}"}
-
when str.ascii_only?
-
str
-
else
-
RubyVer.param_encode(str)
-
end
-
end
-
-
# Decodes a parameter value using URI Escaping.
-
#
-
# Example:
-
#
-
# Mail::Encodings.param_decode("This%20is%20fun", 'us-ascii') #=> "This is fun"
-
#
-
# str = Mail::Encodings.param_decode("This%20is%20fun", 'iso-8559-1')
-
# str.encoding #=> 'ISO-8859-1' ## Only on Ruby 1.9
-
# str #=> "This is fun"
-
2
def Encodings.param_decode(str, encoding)
-
RubyVer.param_decode(str, encoding)
-
end
-
-
# Decodes or encodes a string as needed for either Base64 or QP encoding types in
-
# the =?<encoding>?[QB]?<string>?=" format.
-
#
-
# The output type needs to be :decode to decode the input string or :encode to
-
# encode the input string. The character set used for encoding will either be
-
# the value of $KCODE for Ruby < 1.9 or the encoding on the string passed in.
-
#
-
# On encoding, will only send out Base64 encoded strings.
-
2
def Encodings.decode_encode(str, output_type)
-
case
-
when output_type == :decode
-
Encodings.value_decode(str)
-
else
-
if str.ascii_only?
-
str
-
else
-
Encodings.b_value_encode(str, find_encoding(str))
-
end
-
end
-
end
-
-
# Decodes a given string as Base64 or Quoted Printable, depending on what
-
# type it is.
-
#
-
# String has to be of the format =?<encoding>?[QB]?<string>?=
-
2
def Encodings.value_decode(str)
-
# Optimization: If there's no encoded-words in the string, just return it
-
return str unless str =~ ENCODED_VALUE
-
-
lines = collapse_adjacent_encodings(str)
-
-
# Split on white-space boundaries with capture, so we capture the white-space as well
-
lines.each do |line|
-
line.gsub!(ENCODED_VALUE) do |string|
-
case $2
-
when *B_VALUES then b_value_decode(string)
-
when *Q_VALUES then q_value_decode(string)
-
end
-
end
-
end.join("")
-
end
-
-
# Takes an encoded string of the format =?<encoding>?[QB]?<string>?=
-
2
def Encodings.unquote_and_convert_to(str, to_encoding)
-
output = value_decode( str ).to_s # output is already converted to UTF-8
-
-
if 'utf8' == to_encoding.to_s.downcase.gsub("-", "")
-
output
-
elsif to_encoding
-
begin
-
if RUBY_VERSION >= '1.9'
-
output.encode(to_encoding)
-
else
-
require 'iconv'
-
Iconv.iconv(to_encoding, 'UTF-8', output).first
-
end
-
rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
-
# the 'from' parameter specifies a charset other than what the text
-
# actually is...not much we can do in this case but just return the
-
# unconverted text.
-
#
-
# Ditto if either parameter represents an unknown charset, like
-
# X-UNKNOWN.
-
output
-
end
-
else
-
output
-
end
-
end
-
-
2
def Encodings.address_encode(address, charset = 'utf-8')
-
if address.is_a?(Array)
-
# loop back through for each element
-
address.compact.map { |a| Encodings.address_encode(a, charset) }.join(", ")
-
else
-
# find any word boundary that is not ascii and encode it
-
encode_non_usascii(address, charset) if address
-
end
-
end
-
-
2
def Encodings.encode_non_usascii(address, charset)
-
return address if address.ascii_only? or charset.nil?
-
us_ascii = %Q{\x00-\x7f}
-
# Encode any non usascii strings embedded inside of quotes
-
address = address.gsub(/(".*?[^#{us_ascii}].*?")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
-
# Then loop through all remaining items and encode as needed
-
tokens = address.split(/\s/)
-
map_with_index(tokens) do |word, i|
-
if word.ascii_only?
-
word
-
else
-
previous_non_ascii = i>0 && tokens[i-1] && !tokens[i-1].ascii_only?
-
if previous_non_ascii #why are we adding an extra space here?
-
word = " #{word}"
-
end
-
Encodings.b_value_encode(word, charset)
-
end
-
end.join(' ')
-
end
-
-
# Encode a string with Base64 Encoding and returns it ready to be inserted
-
# as a value for a field, that is, in the =?<charset>?B?<string>?= format
-
#
-
# Example:
-
#
-
# Encodings.b_value_encode('This is あ string', 'UTF-8')
-
# #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
-
2
def Encodings.b_value_encode(encoded_str, encoding = nil)
-
return encoded_str if encoded_str.to_s.ascii_only?
-
string, encoding = RubyVer.b_value_encode(encoded_str, encoding)
-
map_lines(string) do |str|
-
"=?#{encoding}?B?#{str.chomp}?="
-
end.join(" ")
-
end
-
-
# Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
-
# as a value for a field, that is, in the =?<charset>?Q?<string>?= format
-
#
-
# Example:
-
#
-
# Encodings.q_value_encode('This is あ string', 'UTF-8')
-
# #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
-
2
def Encodings.q_value_encode(encoded_str, encoding = nil)
-
return encoded_str if encoded_str.to_s.ascii_only?
-
string, encoding = RubyVer.q_value_encode(encoded_str, encoding)
-
string.gsub!("=\r\n", '') # We already have limited the string to the length we want
-
map_lines(string) do |str|
-
"=?#{encoding}?Q?#{str.chomp.gsub(/ /, '_')}?="
-
end.join(" ")
-
end
-
-
2
private
-
-
# Decodes a Base64 string from the "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=" format
-
#
-
# Example:
-
#
-
# Encodings.b_value_decode("=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=")
-
# #=> 'This is あ string'
-
2
def Encodings.b_value_decode(str)
-
RubyVer.b_value_decode(str)
-
end
-
-
# Decodes a Quoted-Printable string from the "=?UTF-8?Q?This_is_=E3=81=82_string?=" format
-
#
-
# Example:
-
#
-
# Encodings.q_value_decode("=?UTF-8?Q?This_is_=E3=81=82_string?=")
-
# #=> 'This is あ string'
-
2
def Encodings.q_value_decode(str)
-
RubyVer.q_value_decode(str)
-
end
-
-
2
def Encodings.find_encoding(str)
-
RUBY_VERSION >= '1.9' ? str.encoding : $KCODE
-
end
-
-
# Gets the encoding type (Q or B) from the string.
-
2
def Encodings.value_encoding_from_string(str)
-
str[ENCODED_VALUE, 1]
-
end
-
-
# When the encoded string consists of multiple lines, lines with the same
-
# encoding (Q or B) can be joined together.
-
#
-
# String has to be of the format =?<encoding>?[QB]?<string>?=
-
2
def Encodings.collapse_adjacent_encodings(str)
-
results = []
-
previous_encoding = nil
-
lines = str.split(FULL_ENCODED_VALUE)
-
lines.each_slice(2) do |unencoded, encoded|
-
if encoded
-
encoding = value_encoding_from_string(encoded)
-
if encoding == previous_encoding && Utilities.blank?(unencoded)
-
results.last << encoded
-
else
-
results << unencoded unless unencoded == EMPTY
-
results << encoded
-
end
-
previous_encoding = encoding
-
else
-
results << unencoded
-
end
-
end
-
-
results
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/encodings/8bit'
-
-
2
module Mail
-
2
module Encodings
-
2
class SevenBit < EightBit
-
2
NAME = '7bit'
-
-
2
PRIORITY = 1
-
-
# 7bit and 8bit operate the same
-
-
# Decode the string
-
2
def self.decode(str)
-
super
-
end
-
-
# Encode the string
-
2
def self.encode(str)
-
super
-
end
-
-
# Idenity encodings have a fixed cost, 1 byte out per 1 byte in
-
2
def self.cost(str)
-
super
-
end
-
-
2
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/encodings/binary'
-
-
2
module Mail
-
2
module Encodings
-
2
class EightBit < Binary
-
2
NAME = '8bit'
-
-
2
PRIORITY = 4
-
-
# 8bit is an identiy encoding, meaning nothing to do
-
-
# Decode the string
-
2
def self.decode(str)
-
::Mail::Utilities.to_lf str
-
end
-
-
# Encode the string
-
2
def self.encode(str)
-
::Mail::Utilities.to_crlf str
-
end
-
-
# Idenity encodings have a fixed cost, 1 byte out per 1 byte in
-
2
def self.cost(str)
-
1.0
-
end
-
-
# Per RFC 2821 4.5.3.1, SMTP lines may not be longer than 1000 octets including the <CRLF>.
-
2
def self.compatible_input?(str)
-
!str.lines.find { |line| line.length > 998 }
-
end
-
-
2
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/encodings/7bit'
-
-
2
module Mail
-
2
module Encodings
-
2
class Base64 < SevenBit
-
2
NAME = 'base64'
-
-
2
PRIORITY = 3
-
-
2
def self.can_encode?(enc)
-
true
-
end
-
-
# Decode the string from Base64
-
2
def self.decode(str)
-
RubyVer.decode_base64( str )
-
end
-
-
# Encode the string to Base64
-
2
def self.encode(str)
-
::Mail::Utilities.to_crlf(RubyVer.encode_base64( str ))
-
end
-
-
# Base64 has a fixed cost, 4 bytes out per 3 bytes in
-
2
def self.cost(str)
-
4.0/3
-
end
-
-
# Base64 inserts newlines automatically and cannot violate the SMTP spec.
-
2
def self.compatible_input?(str)
-
true
-
end
-
-
2
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/encodings/transfer_encoding'
-
-
2
module Mail
-
2
module Encodings
-
2
class Binary < TransferEncoding
-
2
NAME = 'binary'
-
-
2
PRIORITY = 5
-
-
# Binary is an identiy encoding, meaning nothing to do
-
-
# Decode the string
-
2
def self.decode(str)
-
str
-
end
-
-
# Encode the string
-
2
def self.encode(str)
-
str
-
end
-
-
# Idenity encodings have a fixed cost, 1 byte out per 1 byte in
-
2
def self.cost(str)
-
1.0
-
end
-
-
2
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/encodings/7bit'
-
-
2
module Mail
-
2
module Encodings
-
2
class QuotedPrintable < SevenBit
-
2
NAME='quoted-printable'
-
-
2
PRIORITY = 2
-
-
2
def self.can_encode?(str)
-
EightBit.can_encode? str
-
end
-
-
# Decode the string from Quoted-Printable. Cope with hard line breaks
-
# that were incorrectly encoded as hex instead of literal CRLF.
-
2
def self.decode(str)
-
::Mail::Utilities.to_lf str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first
-
end
-
-
2
def self.encode(str)
-
::Mail::Utilities.to_crlf([::Mail::Utilities.to_lf(str)].pack("M"))
-
end
-
-
2
def self.cost(str)
-
# These bytes probably do not need encoding
-
c = str.count("\x9\xA\xD\x20-\x3C\x3E-\x7E")
-
# Everything else turns into =XX where XX is a
-
# two digit hex number (taking 3 bytes)
-
total = (str.bytesize - c)*3 + c
-
total.to_f/str.bytesize
-
end
-
-
# QP inserts newlines automatically and cannot violate the SMTP spec.
-
2
def self.compatible_input?(str)
-
true
-
end
-
-
2
private
-
-
2
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
2
module Encodings
-
2
class TransferEncoding
-
2
NAME = ''
-
-
2
PRIORITY = -1
-
-
2
def self.can_transport?(enc)
-
enc = Encodings.get_name(enc)
-
if Encodings.defined? enc
-
Encodings.get_encoding(enc).new.is_a? self
-
else
-
false
-
end
-
end
-
-
2
def self.can_encode?(enc)
-
can_transport? enc
-
end
-
-
2
def self.cost(str)
-
raise "Unimplemented"
-
end
-
-
2
def self.compatible_input?(str)
-
true
-
end
-
-
2
def self.to_s
-
self::NAME
-
end
-
-
2
def self.get_best_compatible(source_encoding, str)
-
if self.can_transport?(source_encoding) && self.compatible_input?(str)
-
source_encoding
-
else
-
choices = Encodings.get_all.select do |enc|
-
self.can_transport?(enc) && enc.can_encode?(source_encoding)
-
end
-
-
best = nil
-
best_cost = nil
-
-
choices.each do |enc|
-
# If the current choice cannot be transported safely,
-
# give priority to other choices but allow it to be used as a fallback.
-
this_cost = enc.cost(str) if enc.compatible_input?(str)
-
-
if !best_cost || (this_cost && this_cost < best_cost)
-
best_cost = this_cost
-
best = enc
-
elsif this_cost == best_cost
-
best = enc if enc::PRIORITY < best::PRIORITY
-
end
-
end
-
-
best
-
end
-
end
-
-
2
def to_s
-
self.class.to_s
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
module Encodings
-
2
module UnixToUnix
-
2
NAME = "x-uuencode"
-
-
2
def self.decode(str)
-
str.sub(/\Abegin \d+ [^\n]*\n/, '').unpack('u').first
-
end
-
-
2
def self.encode(str)
-
[str].pack("u")
-
end
-
-
2
Encodings.register(NAME, self)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Mail Envelope
-
#
-
# The Envelope class provides a field for the first line in an
-
# mbox file, that looks like "From mikel@test.lindsaar.net DATETIME"
-
#
-
# This envelope class reads that line, and turns it into an
-
# Envelope.from and Envelope.date for your use.
-
2
module Mail
-
2
class Envelope < StructuredField
-
-
2
def initialize(*args)
-
super(FIELD_NAME, strip_field(FIELD_NAME, args.last))
-
end
-
-
2
def element
-
@element ||= Mail::EnvelopeFromElement.new(value)
-
end
-
-
2
def date
-
::DateTime.parse("#{element.date_time}")
-
end
-
-
2
def from
-
element.address
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
2
require 'mail/fields'
-
-
# encoding: utf-8
-
2
module Mail
-
# Provides a single class to call to create a new structured or unstructured
-
# field. Works out per RFC what field of field it is being given and returns
-
# the correct field of class back on new.
-
#
-
# ===Per RFC 2822
-
#
-
# 2.2. Header Fields
-
#
-
# Header fields are lines composed of a field name, followed by a colon
-
# (":"), followed by a field body, and terminated by CRLF. A field
-
# name MUST be composed of printable US-ASCII characters (i.e.,
-
# characters that have values between 33 and 126, inclusive), except
-
# colon. A field body may be composed of any US-ASCII characters,
-
# except for CR and LF. However, a field body may contain CRLF when
-
# used in header "folding" and "unfolding" as described in section
-
# 2.2.3. All field bodies MUST conform to the syntax described in
-
# sections 3 and 4 of this standard.
-
#
-
2
class Field
-
-
2
include Utilities
-
2
include Comparable
-
-
2
STRUCTURED_FIELDS = %w[ bcc cc content-description content-disposition
-
content-id content-location content-transfer-encoding
-
content-type date from in-reply-to keywords message-id
-
mime-version received references reply-to
-
resent-bcc resent-cc resent-date resent-from
-
resent-message-id resent-sender resent-to
-
return-path sender to ]
-
-
2
KNOWN_FIELDS = STRUCTURED_FIELDS + ['comments', 'subject']
-
-
2
FIELDS_MAP = {
-
"to" => ToField,
-
"cc" => CcField,
-
"bcc" => BccField,
-
"message-id" => MessageIdField,
-
"in-reply-to" => InReplyToField,
-
"references" => ReferencesField,
-
"subject" => SubjectField,
-
"comments" => CommentsField,
-
"keywords" => KeywordsField,
-
"date" => DateField,
-
"from" => FromField,
-
"sender" => SenderField,
-
"reply-to" => ReplyToField,
-
"resent-date" => ResentDateField,
-
"resent-from" => ResentFromField,
-
"resent-sender" => ResentSenderField,
-
"resent-to" => ResentToField,
-
"resent-cc" => ResentCcField,
-
"resent-bcc" => ResentBccField,
-
"resent-message-id" => ResentMessageIdField,
-
"return-path" => ReturnPathField,
-
"received" => ReceivedField,
-
"mime-version" => MimeVersionField,
-
"content-transfer-encoding" => ContentTransferEncodingField,
-
"content-description" => ContentDescriptionField,
-
"content-disposition" => ContentDispositionField,
-
"content-type" => ContentTypeField,
-
"content-id" => ContentIdField,
-
"content-location" => ContentLocationField,
-
}
-
-
2
FIELD_NAME_MAP = FIELDS_MAP.inject({}) do |map, (field, field_klass)|
-
58
map.update(field => field_klass::CAPITALIZED_FIELD)
-
end
-
-
# Generic Field Exception
-
2
class FieldError < StandardError
-
end
-
-
# Raised when a parsing error has occurred (ie, a StructuredField has tried
-
# to parse a field that is invalid or improperly written)
-
2
class ParseError < FieldError #:nodoc:
-
2
attr_accessor :element, :value, :reason
-
-
2
def initialize(element, value, reason)
-
@element = element
-
@value = value
-
@reason = reason
-
super("#{element} can not parse |#{value}|\nReason was: #{reason}")
-
end
-
end
-
-
# Raised when attempting to set a structured field's contents to an invalid syntax
-
2
class SyntaxError < FieldError #:nodoc:
-
end
-
-
# Accepts a string:
-
#
-
# Field.new("field-name: field data")
-
#
-
# Or name, value pair:
-
#
-
# Field.new("field-name", "value")
-
#
-
# Or a name by itself:
-
#
-
# Field.new("field-name")
-
#
-
# Note, does not want a terminating carriage return. Returns
-
# self appropriately parsed. If value is not a string, then
-
# it will be passed through as is, for example, content-type
-
# field can accept an array with the type and a hash of
-
# parameters:
-
#
-
# Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
-
2
def initialize(name, value = nil, charset = 'utf-8')
-
case
-
when name.index(COLON) # Field.new("field-name: field data")
-
@charset = Utilities.blank?(value) ? charset : value
-
@name = name[FIELD_PREFIX]
-
@raw_value = name
-
@value = nil
-
when Utilities.blank?(value) # Field.new("field-name")
-
@name = name
-
@value = nil
-
@raw_value = nil
-
@charset = charset
-
else # Field.new("field-name", "value")
-
@name = name
-
@value = value
-
@raw_value = nil
-
@charset = charset
-
end
-
@name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
-
end
-
-
2
def field=(value)
-
@field = value
-
end
-
-
2
def field
-
_, @value = split(@raw_value) if @raw_value && !@value
-
@field ||= create_field(@name, @value, @charset)
-
end
-
-
2
def name
-
@name
-
end
-
-
2
def value
-
field.value
-
end
-
-
2
def value=(val)
-
@field = create_field(name, val, @charset)
-
end
-
-
2
def to_s
-
field.to_s
-
end
-
-
2
def inspect
-
"#<#{self.class.name} 0x#{(object_id * 2).to_s(16)} #{instance_variables.map do |ivar|
-
"#{ivar}=#{instance_variable_get(ivar).inspect}"
-
end.join(" ")}>"
-
end
-
-
2
def update(name, value)
-
@field = create_field(name, value, @charset)
-
end
-
-
2
def same( other )
-
return false unless other.kind_of?(self.class)
-
match_to_s(other.name, self.name)
-
end
-
-
2
def ==( other )
-
return false unless other.kind_of?(self.class)
-
match_to_s(other.name, self.name) && match_to_s(other.value, self.value)
-
end
-
-
2
def responsible_for?( val )
-
name.to_s.casecmp(val.to_s) == 0
-
end
-
-
2
def <=>( other )
-
self.field_order_id <=> other.field_order_id
-
end
-
-
2
def field_order_id
-
@field_order_id ||= (FIELD_ORDER_LOOKUP[self.name.to_s.downcase] || 100)
-
end
-
-
2
def method_missing(name, *args, &block)
-
field.send(name, *args, &block)
-
end
-
-
2
if RUBY_VERSION >= '1.9.2'
-
2
def respond_to_missing?(method_name, include_private)
-
field.respond_to?(method_name, include_private) || super
-
end
-
else
-
def respond_to?(method_name, include_private = false)
-
field.respond_to?(method_name, include_private) || super
-
end
-
end
-
-
2
FIELD_ORDER = %w[ return-path received
-
resent-date resent-from resent-sender resent-to
-
resent-cc resent-bcc resent-message-id
-
date from sender reply-to to cc bcc
-
message-id in-reply-to references
-
subject comments keywords
-
mime-version content-type content-transfer-encoding
-
content-location content-disposition content-description ]
-
-
2
FIELD_ORDER_LOOKUP = Hash[FIELD_ORDER.each_with_index.to_a]
-
-
2
private
-
-
2
def split(raw_field)
-
match_data = raw_field.mb_chars.match(FIELD_SPLIT)
-
[match_data[1].to_s.mb_chars.strip, match_data[2].to_s.mb_chars.strip.to_s]
-
rescue
-
STDERR.puts "WARNING: Could not parse (and so ignoring) '#{raw_field}'"
-
end
-
-
# 2.2.3. Long Header Fields
-
#
-
# The process of moving from this folded multiple-line representation
-
# of a header field to its single line representation is called
-
# "unfolding". Unfolding is accomplished by simply removing any CRLF
-
# that is immediately followed by WSP. Each header field should be
-
# treated in its unfolded form for further syntactic and semantic
-
# evaluation.
-
2
def unfold(string)
-
string.gsub(/[\r\n \t]+/m, ' ')
-
end
-
-
2
def create_field(name, value, charset)
-
value = unfold(value) if value.is_a?(String)
-
-
begin
-
new_field(name, value, charset)
-
rescue Mail::Field::ParseError => e
-
field = Mail::UnstructuredField.new(name, value)
-
field.errors << [name, value, e]
-
field
-
end
-
end
-
-
2
def new_field(name, value, charset)
-
lower_case_name = name.to_s.downcase
-
if field_klass = FIELDS_MAP[lower_case_name]
-
field_klass.new(value, charset)
-
else
-
OptionalField.new(name, value, charset)
-
end
-
end
-
-
end
-
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
-
# Field List class provides an enhanced array that keeps a list of
-
# email fields in order. And allows you to insert new fields without
-
# having to worry about the order they will appear in.
-
2
class FieldList < Array
-
-
2
include Enumerable
-
-
# Insert the field in sorted order.
-
#
-
# Heavily based on bisect.insort from Python, which is:
-
# Copyright (C) 2001-2013 Python Software Foundation.
-
# Licensed under <http://docs.python.org/license.html>
-
# From <http://hg.python.org/cpython/file/2.7/Lib/bisect.py>
-
2
def <<( new_field )
-
lo = 0
-
hi = size
-
-
while lo < hi
-
mid = (lo + hi).div(2)
-
if new_field < self[mid]
-
hi = mid
-
else
-
lo = mid + 1
-
end
-
end
-
-
insert(lo, new_field)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
register_autoload :UnstructuredField, 'mail/fields/unstructured_field'
-
2
register_autoload :StructuredField, 'mail/fields/structured_field'
-
2
register_autoload :OptionalField, 'mail/fields/optional_field'
-
-
2
register_autoload :BccField, 'mail/fields/bcc_field'
-
2
register_autoload :CcField, 'mail/fields/cc_field'
-
2
register_autoload :CommentsField, 'mail/fields/comments_field'
-
2
register_autoload :ContentDescriptionField, 'mail/fields/content_description_field'
-
2
register_autoload :ContentDispositionField, 'mail/fields/content_disposition_field'
-
2
register_autoload :ContentIdField, 'mail/fields/content_id_field'
-
2
register_autoload :ContentLocationField, 'mail/fields/content_location_field'
-
2
register_autoload :ContentTransferEncodingField, 'mail/fields/content_transfer_encoding_field'
-
2
register_autoload :ContentTypeField, 'mail/fields/content_type_field'
-
2
register_autoload :DateField, 'mail/fields/date_field'
-
2
register_autoload :FromField, 'mail/fields/from_field'
-
2
register_autoload :InReplyToField, 'mail/fields/in_reply_to_field'
-
2
register_autoload :KeywordsField, 'mail/fields/keywords_field'
-
2
register_autoload :MessageIdField, 'mail/fields/message_id_field'
-
2
register_autoload :MimeVersionField, 'mail/fields/mime_version_field'
-
2
register_autoload :ReceivedField, 'mail/fields/received_field'
-
2
register_autoload :ReferencesField, 'mail/fields/references_field'
-
2
register_autoload :ReplyToField, 'mail/fields/reply_to_field'
-
2
register_autoload :ResentBccField, 'mail/fields/resent_bcc_field'
-
2
register_autoload :ResentCcField, 'mail/fields/resent_cc_field'
-
2
register_autoload :ResentDateField, 'mail/fields/resent_date_field'
-
2
register_autoload :ResentFromField, 'mail/fields/resent_from_field'
-
2
register_autoload :ResentMessageIdField, 'mail/fields/resent_message_id_field'
-
2
register_autoload :ResentSenderField, 'mail/fields/resent_sender_field'
-
2
register_autoload :ResentToField, 'mail/fields/resent_to_field'
-
2
register_autoload :ReturnPathField, 'mail/fields/return_path_field'
-
2
register_autoload :SenderField, 'mail/fields/sender_field'
-
2
register_autoload :SubjectField, 'mail/fields/subject_field'
-
2
register_autoload :ToField, 'mail/fields/to_field'
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Blind Carbon Copy Field
-
#
-
# The Bcc field inherits from StructuredField and handles the Bcc: header
-
# field in the email.
-
#
-
# Sending bcc to a mail message will instantiate a Mail::Field object that
-
# has a BccField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Bcc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.bcc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:bcc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::BccField:0x180e1c4
-
# mail['bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::BccField:0x180e1c4
-
# mail['Bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::BccField:0x180e1c4
-
#
-
# mail[:bcc].encoded #=> '' # Bcc field does not get output into an email
-
# mail[:bcc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:bcc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:bcc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class BccField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'bcc'
-
2
CAPITALIZED_FIELD = 'Bcc'
-
-
2
def initialize(value = '', charset = 'utf-8')
-
@charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def include_in_headers=(include_in_headers)
-
@include_in_headers = include_in_headers
-
end
-
-
2
def include_in_headers
-
defined?(@include_in_headers) ? @include_in_headers : self.include_in_headers = false
-
end
-
-
# Bcc field should not be :encoded by default
-
2
def encoded
-
if include_in_headers
-
do_encode(CAPITALIZED_FIELD)
-
else
-
''
-
end
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Carbon Copy Field
-
#
-
# The Cc field inherits from StructuredField and handles the Cc: header
-
# field in the email.
-
#
-
# Sending cc to a mail message will instantiate a Mail::Field object that
-
# has a CcField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Cc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.cc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:cc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CcField:0x180e1c4
-
# mail['cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CcField:0x180e1c4
-
# mail['Cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CcField:0x180e1c4
-
#
-
# mail[:cc].encoded #=> 'Cc: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:cc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:cc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:cc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class CcField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'cc'
-
2
CAPITALIZED_FIELD = 'Cc'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Comments Field
-
#
-
# The Comments field inherits from UnstructuredField and handles the Comments:
-
# header field in the email.
-
#
-
# Sending comments to a mail message will instantiate a Mail::Field object that
-
# has a CommentsField as its field type.
-
#
-
# An email header can have as many comments fields as it wants. There is no upper
-
# limit, the comments field is also optional (that is, no comment is needed)
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.comments = 'This is a comment'
-
# mail.comments #=> 'This is a comment'
-
# mail[:comments] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CommentsField:0x180e1c4
-
# mail['comments'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CommentsField:0x180e1c4
-
# mail['comments'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::CommentsField:0x180e1c4
-
#
-
# mail.comments = "This is another comment"
-
# mail[:comments].map { |c| c.to_s }
-
# #=> ['This is a comment', "This is another comment"]
-
#
-
2
module Mail
-
2
class CommentsField < UnstructuredField
-
-
2
FIELD_NAME = 'comments'
-
2
CAPITALIZED_FIELD = 'Comments'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
@charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value))
-
self.parse
-
self
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
-
2
class AddressContainer < Array
-
-
2
def initialize(field, list = [])
-
@field = field
-
super(list)
-
end
-
-
2
def << (address)
-
@field << address
-
end
-
-
end
-
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/fields/common/address_container'
-
-
2
module Mail
-
2
module CommonAddress # :nodoc:
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@address_list = AddressList.new(encode_if_needed(val))
-
else
-
nil
-
end
-
end
-
-
2
def charset
-
@charset
-
end
-
-
2
def encode_if_needed(val)
-
Encodings.address_encode(val, charset)
-
end
-
-
# Allows you to iterate through each address object in the address_list
-
2
def each
-
address_list.addresses.each do |address|
-
yield(address)
-
end
-
end
-
-
# Returns the address string of all the addresses in the address list
-
2
def addresses
-
list = address_list.addresses.map { |a| a.address }
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns the formatted string of all the addresses in the address list
-
2
def formatted
-
list = address_list.addresses.map { |a| a.format }
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns the display name of all the addresses in the address list
-
2
def display_names
-
list = address_list.addresses.map { |a| a.display_name }
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns the actual address objects in the address list
-
2
def addrs
-
list = address_list.addresses
-
Mail::AddressContainer.new(self, list)
-
end
-
-
# Returns a hash of group name => address strings for the address list
-
2
def groups
-
address_list.addresses_grouped_by_group
-
end
-
-
# Returns the addresses that are part of groups
-
2
def group_addresses
-
decoded_group_addresses
-
end
-
-
# Returns a list of decoded group addresses
-
2
def decoded_group_addresses
-
groups.map { |k,v| v.map { |a| a.decoded } }.flatten
-
end
-
-
# Returns a list of encoded group addresses
-
2
def encoded_group_addresses
-
groups.map { |k,v| v.map { |a| a.encoded } }.flatten
-
end
-
-
# Returns the name of all the groups in a string
-
2
def group_names # :nodoc:
-
address_list.group_names
-
end
-
-
2
def default
-
addresses
-
end
-
-
2
def <<(val)
-
case
-
when val.nil?
-
raise ArgumentError, "Need to pass an address to <<"
-
when Utilities.blank?(val)
-
parse(encoded)
-
else
-
self.value = [self.value, val].reject {|a| Utilities.blank?(a) }.join(", ")
-
end
-
end
-
-
2
def value=(val)
-
super
-
parse(self.value)
-
end
-
-
2
private
-
-
2
def do_encode(field_name)
-
return '' if Utilities.blank?(value)
-
address_array = address_list.addresses.reject { |a| encoded_group_addresses.include?(a.encoded) }.compact.map { |a| a.encoded }
-
address_text = address_array.join(", \r\n\s")
-
group_array = groups.map { |k,v| "#{k}: #{v.map { |a| a.encoded }.join(", \r\n\s")};" }
-
group_text = group_array.join(" \r\n\s")
-
return_array = [address_text, group_text].reject { |a| Utilities.blank?(a) }
-
"#{field_name}: #{return_array.join(", \r\n\s")}\r\n"
-
end
-
-
2
def do_decode
-
return nil if Utilities.blank?(value)
-
address_array = address_list.addresses.reject { |a| decoded_group_addresses.include?(a.decoded) }.map { |a| a.decoded }
-
address_text = address_array.join(", ")
-
group_array = groups.map { |k,v| "#{k}: #{v.map { |a| a.decoded }.join(", ")};" }
-
group_text = group_array.join(" ")
-
return_array = [address_text, group_text].reject { |a| Utilities.blank?(a) }
-
return_array.join(", ")
-
end
-
-
2
def address_list # :nodoc:
-
@address_list ||= AddressList.new(value)
-
end
-
-
2
def get_group_addresses(group_list)
-
if group_list.respond_to?(:addresses)
-
group_list.addresses.map do |address|
-
Mail::Address.new(address)
-
end
-
else
-
[]
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
2
module CommonDate # :nodoc:
-
# Returns a date time object of the parsed date
-
2
def date_time
-
::DateTime.parse("#{element.date_string} #{element.time_string}")
-
end
-
-
2
def default
-
date_time
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::DateTimeElement.new(val)
-
else
-
nil
-
end
-
end
-
-
2
private
-
-
2
def do_encode(field_name)
-
"#{field_name}: #{value}\r\n"
-
end
-
-
2
def do_decode
-
"#{value}"
-
end
-
-
2
def element
-
@element ||= Mail::DateTimeElement.new(value)
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
2
module CommonField # :nodoc:
-
2
include Mail::Constants
-
-
2
def name=(value)
-
@name = value
-
end
-
-
2
def name
-
@name ||= nil
-
end
-
-
2
def value=(value)
-
@length = nil
-
@tree = nil
-
@element = nil
-
@value = value
-
end
-
-
2
def value
-
@value
-
end
-
-
2
def to_s
-
decoded.to_s
-
end
-
-
2
def default
-
decoded
-
end
-
-
2
def field_length
-
@length ||= "#{name}: #{encode(decoded)}".length
-
end
-
-
2
def responsible_for?( val )
-
name.to_s.casecmp(val.to_s) == 0
-
end
-
-
2
private
-
-
2
def strip_field(field_name, value)
-
if value.is_a?(Array)
-
value
-
else
-
value.to_s.sub(/\A#{field_name}:\s+/i, EMPTY)
-
end
-
end
-
-
2
FILENAME_RE = /\b(filename|name)=([^;"\r\n]+\s[^;"\r\n]+)/
-
2
def ensure_filename_quoted(value)
-
if value.is_a?(String)
-
value.sub FILENAME_RE, '\1="\2"'
-
else
-
value
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
2
module CommonMessageId # :nodoc:
-
2
def element
-
@element ||= Mail::MessageIdsElement.new(value) unless Utilities.blank?(value)
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::MessageIdsElement.new(val)
-
else
-
nil
-
end
-
end
-
-
2
def message_id
-
element.message_id if element
-
end
-
-
2
def message_ids
-
element.message_ids if element
-
end
-
-
2
def default
-
return nil unless message_ids
-
if message_ids.length == 1
-
message_ids[0]
-
else
-
message_ids
-
end
-
end
-
-
2
private
-
-
2
def do_encode(field_name)
-
%Q{#{field_name}: #{formated_message_ids("\r\n ")}\r\n}
-
end
-
-
2
def do_decode
-
formated_message_ids(' ')
-
end
-
-
2
def formated_message_ids(join)
-
message_ids.map{ |m| "<#{m}>" }.join(join) if message_ids
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
-
# ParameterHash is an intelligent Hash that allows you to add
-
# parameter values including the MIME extension paramaters that
-
# have the name*0="blah", name*1="bleh" keys, and will just return
-
# a single key called name="blahbleh" and do any required un-encoding
-
# to make that happen
-
# Parameters are defined in RFC2045, split keys are in RFC2231
-
-
2
class ParameterHash < IndifferentHash
-
-
2
include Mail::Utilities
-
-
2
def [](key_name)
-
key_pattern = Regexp.escape(key_name.to_s)
-
pairs = []
-
exact = nil
-
each do |k,v|
-
if k =~ /^#{key_pattern}(\*|$)/i
-
if $1 == ASTERISK
-
pairs << [k, v]
-
else
-
exact = k
-
end
-
end
-
end
-
if pairs.empty? # Just dealing with a single value pair
-
super(exact || key_name)
-
else # Dealing with a multiple value pair or a single encoded value pair
-
string = pairs.sort { |a,b| a.first.to_s <=> b.first.to_s }.map { |v| v.last }.join('')
-
if mt = string.match(/([\w\-]+)'(\w\w)'(.*)/)
-
string = mt[3]
-
encoding = mt[1]
-
else
-
encoding = nil
-
end
-
Mail::Encodings.param_decode(string, encoding)
-
end
-
end
-
-
2
def encoded
-
map.sort_by { |a| a.first.to_s }.map! do |key_name, value|
-
unless value.ascii_only?
-
value = Mail::Encodings.param_encode(value)
-
key_name = "#{key_name}*"
-
end
-
%Q{#{key_name}=#{quote_token(value)}}
-
end.join(";\r\n\s")
-
end
-
-
2
def decoded
-
map.sort_by { |a| a.first.to_s }.map! do |key_name, value|
-
%Q{#{key_name}=#{quote_token(value)}}
-
end.join("; ")
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
2
module Mail
-
2
class ContentDescriptionField < UnstructuredField
-
-
2
FIELD_NAME = 'content-description'
-
2
CAPITALIZED_FIELD = 'Content-Description'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/fields/common/parameter_hash'
-
-
2
module Mail
-
2
class ContentDispositionField < StructuredField
-
-
2
FIELD_NAME = 'content-disposition'
-
2
CAPITALIZED_FIELD = 'Content-Disposition'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = ensure_filename_quoted(value)
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ContentDispositionElement.new(val)
-
end
-
end
-
-
2
def element
-
@element ||= Mail::ContentDispositionElement.new(value)
-
end
-
-
2
def disposition_type
-
element.disposition_type
-
end
-
-
2
def parameters
-
@parameters = ParameterHash.new
-
element.parameters.each { |p| @parameters.merge!(p) } unless element.parameters.nil?
-
@parameters
-
end
-
-
2
def filename
-
case
-
when !Utilities.blank?(parameters['filename'])
-
@filename = parameters['filename']
-
when !Utilities.blank?(parameters['name'])
-
@filename = parameters['name']
-
else
-
@filename = nil
-
end
-
@filename
-
end
-
-
# TODO: Fix this up
-
2
def encoded
-
if parameters.length > 0
-
p = ";\r\n\s#{parameters.encoded}\r\n"
-
else
-
p = "\r\n"
-
end
-
"#{CAPITALIZED_FIELD}: #{disposition_type}" + p
-
end
-
-
2
def decoded
-
if parameters.length > 0
-
p = "; #{parameters.decoded}"
-
else
-
p = ""
-
end
-
"#{disposition_type}" + p
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
2
module Mail
-
2
class ContentIdField < StructuredField
-
-
2
FIELD_NAME = 'content-id'
-
2
CAPITALIZED_FIELD = "Content-ID"
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
@uniq = 1
-
if Utilities.blank?(value)
-
value = generate_content_id
-
else
-
value = strip_field(FIELD_NAME, value)
-
end
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::MessageIdsElement.new(val)
-
end
-
end
-
-
2
def element
-
@element ||= Mail::MessageIdsElement.new(value)
-
end
-
-
2
def name
-
'Content-ID'
-
end
-
-
2
def content_id
-
element.message_id
-
end
-
-
2
def to_s
-
"<#{content_id}>"
-
end
-
-
# TODO: Fix this up
-
2
def encoded
-
"#{CAPITALIZED_FIELD}: #{to_s}\r\n"
-
end
-
-
2
def decoded
-
"#{to_s}"
-
end
-
-
2
private
-
-
2
def generate_content_id
-
"<#{Mail.random_tag}@#{::Socket.gethostname}.mail>"
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
2
module Mail
-
2
class ContentLocationField < StructuredField
-
-
2
FIELD_NAME = 'content-location'
-
2
CAPITALIZED_FIELD = 'Content-Location'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ContentLocationElement.new(val)
-
end
-
end
-
-
2
def element
-
@element ||= Mail::ContentLocationElement.new(value)
-
end
-
-
2
def location
-
element.location
-
end
-
-
# TODO: Fix this up
-
2
def encoded
-
"#{CAPITALIZED_FIELD}: #{location}\r\n"
-
end
-
-
2
def decoded
-
location
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
2
module Mail
-
2
class ContentTransferEncodingField < StructuredField
-
-
2
FIELD_NAME = 'content-transfer-encoding'
-
2
CAPITALIZED_FIELD = 'Content-Transfer-Encoding'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = '7bit' if value.to_s =~ /7-?bits?/i
-
value = '8bit' if value.to_s =~ /8-?bits?/i
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ContentTransferEncodingElement.new(val)
-
end
-
end
-
-
2
def element
-
@element ||= Mail::ContentTransferEncodingElement.new(value)
-
end
-
-
2
def encoding
-
element.encoding
-
end
-
-
# TODO: Fix this up
-
2
def encoded
-
"#{CAPITALIZED_FIELD}: #{encoding}\r\n"
-
end
-
-
2
def decoded
-
encoding
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/fields/common/parameter_hash'
-
-
2
module Mail
-
2
class ContentTypeField < StructuredField
-
-
2
FIELD_NAME = 'content-type'
-
2
CAPITALIZED_FIELD = 'Content-Type'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if value.class == Array
-
@main_type = value[0]
-
@sub_type = value[1]
-
@parameters = ParameterHash.new.merge!(value.last)
-
else
-
@main_type = nil
-
@sub_type = nil
-
@parameters = nil
-
value = strip_field(FIELD_NAME, value)
-
end
-
value = ensure_filename_quoted(value)
-
super(CAPITALIZED_FIELD, value, charset)
-
self.parse
-
self
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
self.value = val
-
@element = nil
-
element
-
end
-
end
-
-
2
def element
-
begin
-
@element ||= Mail::ContentTypeElement.new(value)
-
rescue
-
attempt_to_clean
-
end
-
end
-
-
2
def attempt_to_clean
-
# Sanitize the value, handle special cases
-
@element ||= Mail::ContentTypeElement.new(sanatize(value))
-
rescue
-
# All else fails, just get the MIME media type
-
@element ||= Mail::ContentTypeElement.new(get_mime_type(value))
-
end
-
-
2
def main_type
-
@main_type ||= element.main_type
-
end
-
-
2
def sub_type
-
@sub_type ||= element.sub_type
-
end
-
-
2
def string
-
"#{main_type}/#{sub_type}"
-
end
-
-
2
def default
-
decoded
-
end
-
-
2
alias :content_type :string
-
-
2
def parameters
-
unless @parameters
-
@parameters = ParameterHash.new
-
element.parameters.each { |p| @parameters.merge!(p) }
-
end
-
@parameters
-
end
-
-
2
def ContentTypeField.with_boundary(type)
-
new("#{type}; boundary=#{generate_boundary}")
-
end
-
-
2
def ContentTypeField.generate_boundary
-
"--==_mimepart_#{Mail.random_tag}"
-
end
-
-
2
def value
-
if @value.class == Array
-
"#{@main_type}/#{@sub_type}; #{stringify(parameters)}"
-
else
-
@value
-
end
-
end
-
-
2
def stringify(params)
-
params.map { |k,v| "#{k}=#{Encodings.param_encode(v)}" }.join("; ")
-
end
-
-
2
def filename
-
case
-
when parameters['filename']
-
@filename = parameters['filename']
-
when parameters['name']
-
@filename = parameters['name']
-
else
-
@filename = nil
-
end
-
@filename
-
end
-
-
# TODO: Fix this up
-
2
def encoded
-
if parameters.length > 0
-
p = ";\r\n\s#{parameters.encoded}"
-
else
-
p = ""
-
end
-
"#{CAPITALIZED_FIELD}: #{content_type}#{p}\r\n"
-
end
-
-
2
def decoded
-
if parameters.length > 0
-
p = "; #{parameters.decoded}"
-
else
-
p = ""
-
end
-
"#{content_type}" + p
-
end
-
-
2
private
-
-
2
def method_missing(name, *args, &block)
-
if name.to_s =~ /(\w+)=/
-
self.parameters[$1] = args.first
-
@value = "#{content_type}; #{stringify(parameters)}"
-
else
-
super
-
end
-
end
-
-
# Various special cases from random emails found that I am not going to change
-
# the parser for
-
2
def sanatize( val )
-
-
# TODO: check if there are cases where whitespace is not a separator
-
val = val.
-
gsub(/\s*=\s*/,'='). # remove whitespaces around equal sign
-
tr(' ',';').
-
squeeze(';').
-
gsub(';', '; '). #use '; ' as a separator (or EOL)
-
gsub(/;\s*$/,'') #remove trailing to keep examples below
-
-
if val =~ /(boundary=(\S*))/i
-
val = "#{$`.downcase}boundary=#{$2}#{$'.downcase}"
-
else
-
val.downcase!
-
end
-
-
case
-
when val.chomp =~ /^\s*([\w\-]+)\/([\w\-]+)\s*;;+(.*)$/i
-
# Handles 'text/plain;; format="flowed"' (double semi colon)
-
"#{$1}/#{$2}; #{$3}"
-
when val.chomp =~ /^\s*([\w\-]+)\/([\w\-]+)\s*;\s?(ISO[\w\-]+)$/i
-
# Microsoft helper:
-
# Handles 'type/subtype;ISO-8559-1'
-
"#{$1}/#{$2}; charset=#{quote_atom($3)}"
-
when val.chomp =~ /^text;?$/i
-
# Handles 'text;' and 'text'
-
"text/plain;"
-
when val.chomp =~ /^(\w+);\s(.*)$/i
-
# Handles 'text; <parameters>'
-
"text/plain; #{$2}"
-
when val =~ /([\w\-]+\/[\w\-]+);\scharset="charset="(\w+)""/i
-
# Handles text/html; charset="charset="GB2312""
-
"#{$1}; charset=#{quote_atom($2)}"
-
when val =~ /([\w\-]+\/[\w\-]+);\s+(.*)/i
-
type = $1
-
# Handles misquoted param values
-
# e.g: application/octet-stream; name=archiveshelp1[1].htm
-
# and: audio/x-midi;\r\n\sname=Part .exe
-
params = $2.to_s.split(/\s+/)
-
params = params.map { |i| i.to_s.chomp.strip }
-
params = params.map { |i| i.split(/\s*\=\s*/) }
-
params = params.map { |i| "#{i[0]}=#{dquote(i[1].to_s.gsub(/;$/,""))}" }.join('; ')
-
"#{type}; #{params}"
-
when val =~ /^\s*$/
-
'text/plain'
-
else
-
''
-
end
-
end
-
-
2
def get_mime_type( val )
-
case
-
when val =~ /^([\w\-]+)\/([\w\-]+);.+$/i
-
"#{$1}/#{$2}"
-
else
-
'text/plain'
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Date Field
-
#
-
# The Date field inherits from StructuredField and handles the Date: header
-
# field in the email.
-
#
-
# Sending date to a mail message will instantiate a Mail::Field object that
-
# has a DateField as its field type. This includes all Mail::CommonAddress
-
# module instance methods.
-
#
-
# There must be excatly one Date field in an RFC2822 email.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.date = 'Mon, 24 Nov 1997 14:22:01 -0800'
-
# mail.date #=> #<DateTime: 211747170121/86400,-1/3,2299161>
-
# mail.date.to_s #=> 'Mon, 24 Nov 1997 14:22:01 -0800'
-
# mail[:date] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::DateField:0x180e1c4
-
# mail['date'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::DateField:0x180e1c4
-
# mail['Date'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::DateField:0x180e1c4
-
#
-
2
require 'mail/fields/common/common_date'
-
-
2
module Mail
-
2
class DateField < StructuredField
-
-
2
include Mail::CommonDate
-
-
2
FIELD_NAME = 'date'
-
2
CAPITALIZED_FIELD = "Date"
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if Utilities.blank?(value)
-
value = ::DateTime.now.strftime('%a, %d %b %Y %H:%M:%S %z')
-
else
-
value = strip_field(FIELD_NAME, value)
-
value.to_s.gsub!(/\(.*?\)/, '')
-
value = ::DateTime.parse(value.to_s.squeeze(" ")).strftime('%a, %d %b %Y %H:%M:%S %z')
-
end
-
super(CAPITALIZED_FIELD, value, charset)
-
rescue ArgumentError => e
-
raise e unless "invalid date"==e.message
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = From Field
-
#
-
# The From field inherits from StructuredField and handles the From: header
-
# field in the email.
-
#
-
# Sending from to a mail message will instantiate a Mail::Field object that
-
# has a FromField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one From field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.from = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:from] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::FromField:0x180e1c4
-
# mail['from'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::FromField:0x180e1c4
-
# mail['From'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::FromField:0x180e1c4
-
#
-
# mail[:from].encoded #=> 'from: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:from].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:from].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:from].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class FromField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'from'
-
2
CAPITALIZED_FIELD = 'From'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = In-Reply-To Field
-
#
-
# The In-Reply-To field inherits from StructuredField and handles the
-
# In-Reply-To: header field in the email.
-
#
-
# Sending in_reply_to to a mail message will instantiate a Mail::Field object that
-
# has a InReplyToField as its field type. This includes all Mail::CommonMessageId
-
# module instance metods.
-
#
-
# Note that, the #message_ids method will return an array of message IDs without the
-
# enclosing angle brackets which per RFC are not syntactically part of the message id.
-
#
-
# Only one InReplyTo field can appear in a header, though it can have multiple
-
# Message IDs.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.in_reply_to = '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail.in_reply_to #=> '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail[:in_reply_to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::InReplyToField:0x180e1c4
-
# mail['in_reply_to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::InReplyToField:0x180e1c4
-
# mail['In-Reply-To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::InReplyToField:0x180e1c4
-
#
-
# mail[:in_reply_to].message_ids #=> ['F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom']
-
#
-
2
require 'mail/fields/common/common_message_id'
-
-
2
module Mail
-
2
class InReplyToField < StructuredField
-
-
2
include Mail::CommonMessageId
-
-
2
FIELD_NAME = 'in-reply-to'
-
2
CAPITALIZED_FIELD = 'In-Reply-To'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = value.join("\r\n\s") if value.is_a?(Array)
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# keywords = "Keywords:" phrase *("," phrase) CRLF
-
2
module Mail
-
2
class KeywordsField < StructuredField
-
-
2
FIELD_NAME = 'keywords'
-
2
CAPITALIZED_FIELD = 'Keywords'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@phrase_list ||= PhraseList.new(value)
-
end
-
end
-
-
2
def phrase_list
-
@phrase_list ||= PhraseList.new(value)
-
end
-
-
2
def keywords
-
phrase_list.phrases
-
end
-
-
2
def encoded
-
"#{CAPITALIZED_FIELD}: #{keywords.join(",\r\n ")}\r\n"
-
end
-
-
2
def decoded
-
keywords.join(', ')
-
end
-
-
2
def default
-
keywords
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Message-ID Field
-
#
-
# The Message-ID field inherits from StructuredField and handles the
-
# Message-ID: header field in the email.
-
#
-
# Sending message_id to a mail message will instantiate a Mail::Field object that
-
# has a MessageIdField as its field type. This includes all Mail::CommonMessageId
-
# module instance metods.
-
#
-
# Only one MessageId field can appear in a header, and syntactically it can only have
-
# one Message ID. The message_ids method call has been left in however as it will only
-
# return the one message id, ie, an array of length 1.
-
#
-
# Note that, the #message_ids method will return an array of message IDs without the
-
# enclosing angle brackets which per RFC are not syntactically part of the message id.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.message_id = '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail.message_id #=> '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail[:message_id] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::MessageIdField:0x180e1c4
-
# mail['message_id'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::MessageIdField:0x180e1c4
-
# mail['Message-ID'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::MessageIdField:0x180e1c4
-
#
-
# mail[:message_id].message_id #=> 'F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom'
-
# mail[:message_id].message_ids #=> ['F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom']
-
#
-
2
require 'mail/fields/common/common_message_id'
-
-
2
module Mail
-
2
class MessageIdField < StructuredField
-
-
2
include Mail::CommonMessageId
-
-
2
FIELD_NAME = 'message-id'
-
2
CAPITALIZED_FIELD = 'Message-ID'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
@uniq = 1
-
if Utilities.blank?(value)
-
self.name = CAPITALIZED_FIELD
-
self.value = generate_message_id
-
else
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
end
-
self.parse
-
self
-
-
end
-
-
2
def name
-
'Message-ID'
-
end
-
-
2
def message_ids
-
[message_id]
-
end
-
-
2
def to_s
-
"<#{message_id}>"
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
2
private
-
-
2
def generate_message_id
-
"<#{Mail.random_tag}@#{::Socket.gethostname}.mail>"
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
#
-
#
-
2
module Mail
-
2
class MimeVersionField < StructuredField
-
-
2
FIELD_NAME = 'mime-version'
-
2
CAPITALIZED_FIELD = 'Mime-Version'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if Utilities.blank?(value)
-
value = '1.0'
-
end
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::MimeVersionElement.new(val)
-
end
-
end
-
-
2
def element
-
@element ||= Mail::MimeVersionElement.new(value)
-
end
-
-
2
def version
-
"#{element.major}.#{element.minor}"
-
end
-
-
2
def major
-
element.major.to_i
-
end
-
-
2
def minor
-
element.minor.to_i
-
end
-
-
2
def encoded
-
"#{CAPITALIZED_FIELD}: #{version}\r\n"
-
end
-
-
2
def decoded
-
version
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# trace = [return]
-
# 1*received
-
#
-
# return = "Return-Path:" path CRLF
-
#
-
# path = ([CFWS] "<" ([CFWS] / addr-spec) ">" [CFWS]) /
-
# obs-path
-
#
-
# received = "Received:" name-val-list ";" date-time CRLF
-
#
-
# name-val-list = [CFWS] [name-val-pair *(CFWS name-val-pair)]
-
#
-
# name-val-pair = item-name CFWS item-value
-
#
-
# item-name = ALPHA *(["-"] (ALPHA / DIGIT))
-
#
-
# item-value = 1*angle-addr / addr-spec /
-
# atom / domain / msg-id
-
#
-
2
module Mail
-
2
class ReceivedField < StructuredField
-
-
2
FIELD_NAME = 'received'
-
2
CAPITALIZED_FIELD = 'Received'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
-
end
-
-
2
def parse(val = value)
-
unless Utilities.blank?(val)
-
@element = Mail::ReceivedElement.new(val)
-
end
-
end
-
-
2
def element
-
@element ||= Mail::ReceivedElement.new(value)
-
end
-
-
2
def date_time
-
@datetime ||= ::DateTime.parse("#{element.date_time}")
-
end
-
-
2
def info
-
element.info
-
end
-
-
2
def formatted_date
-
date_time.strftime("%a, %d %b %Y %H:%M:%S ") + date_time.zone.delete(':')
-
end
-
-
2
def encoded
-
if Utilities.blank?(value)
-
"#{CAPITALIZED_FIELD}: \r\n"
-
else
-
"#{CAPITALIZED_FIELD}: #{info}; #{formatted_date}\r\n"
-
end
-
end
-
-
2
def decoded
-
if Utilities.blank?(value)
-
""
-
else
-
"#{info}; #{formatted_date}"
-
end
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = References Field
-
#
-
# The References field inherits references StructuredField and handles the References: header
-
# field in the email.
-
#
-
# Sending references to a mail message will instantiate a Mail::Field object that
-
# has a ReferencesField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Note that, the #message_ids method will return an array of message IDs without the
-
# enclosing angle brackets which per RFC are not syntactically part of the message id.
-
#
-
# Only one References field can appear in a header, though it can have multiple
-
# Message IDs.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.references = '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail.references #=> '<F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom>'
-
# mail[:references] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReferencesField:0x180e1c4
-
# mail['references'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReferencesField:0x180e1c4
-
# mail['References'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReferencesField:0x180e1c4
-
#
-
# mail[:references].message_ids #=> ['F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@test.me.dom']
-
#
-
2
require 'mail/fields/common/common_message_id'
-
-
2
module Mail
-
2
class ReferencesField < StructuredField
-
-
2
include CommonMessageId
-
-
2
FIELD_NAME = 'references'
-
2
CAPITALIZED_FIELD = 'References'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
value = value.join("\r\n\s") if value.is_a?(Array)
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Reply-To Field
-
#
-
# The Reply-To field inherits reply-to StructuredField and handles the Reply-To: header
-
# field in the email.
-
#
-
# Sending reply_to to a mail message will instantiate a Mail::Field object that
-
# has a ReplyToField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Reply-To field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.reply_to = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:reply_to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReplyToField:0x180e1c4
-
# mail['reply-to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReplyToField:0x180e1c4
-
# mail['Reply-To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ReplyToField:0x180e1c4
-
#
-
# mail[:reply_to].encoded #=> 'Reply-To: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:reply_to].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:reply_to].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:reply_to].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ReplyToField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'reply-to'
-
2
CAPITALIZED_FIELD = 'Reply-To'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-Bcc Field
-
#
-
# The Resent-Bcc field inherits resent-bcc StructuredField and handles the
-
# Resent-Bcc: header field in the email.
-
#
-
# Sending resent_bcc to a mail message will instantiate a Mail::Field object that
-
# has a ResentBccField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-Bcc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_bcc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_bcc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentBccField:0x180e1c4
-
# mail['resent-bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentBccField:0x180e1c4
-
# mail['Resent-Bcc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentBccField:0x180e1c4
-
#
-
# mail[:resent_bcc].encoded #=> 'Resent-Bcc: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_bcc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_bcc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_bcc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ResentBccField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'resent-bcc'
-
2
CAPITALIZED_FIELD = 'Resent-Bcc'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-Cc Field
-
#
-
# The Resent-Cc field inherits resent-cc StructuredField and handles the Resent-Cc: header
-
# field in the email.
-
#
-
# Sending resent_cc to a mail message will instantiate a Mail::Field object that
-
# has a ResentCcField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-Cc field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_cc = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_cc] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentCcField:0x180e1c4
-
# mail['resent-cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentCcField:0x180e1c4
-
# mail['Resent-Cc'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentCcField:0x180e1c4
-
#
-
# mail[:resent_cc].encoded #=> 'Resent-Cc: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_cc].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_cc].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_cc].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ResentCcField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'resent-cc'
-
2
CAPITALIZED_FIELD = 'Resent-Cc'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# resent-date = "Resent-Date:" date-time CRLF
-
2
require 'mail/fields/common/common_date'
-
-
2
module Mail
-
2
class ResentDateField < StructuredField
-
-
2
include Mail::CommonDate
-
-
2
FIELD_NAME = 'resent-date'
-
2
CAPITALIZED_FIELD = 'Resent-Date'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
if Utilities.blank?(value)
-
value = ::DateTime.now.strftime('%a, %d %b %Y %H:%M:%S %z')
-
else
-
value = strip_field(FIELD_NAME, value)
-
value = ::DateTime.parse(value.to_s).strftime('%a, %d %b %Y %H:%M:%S %z')
-
end
-
super(CAPITALIZED_FIELD, value, charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-From Field
-
#
-
# The Resent-From field inherits resent-from StructuredField and handles the Resent-From: header
-
# field in the email.
-
#
-
# Sending resent_from to a mail message will instantiate a Mail::Field object that
-
# has a ResentFromField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-From field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_from = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_from] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentFromField:0x180e1c4
-
# mail['resent-from'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentFromField:0x180e1c4
-
# mail['Resent-From'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentFromField:0x180e1c4
-
#
-
# mail[:resent_from].encoded #=> 'Resent-From: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_from].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_from].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_from].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ResentFromField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'resent-from'
-
2
CAPITALIZED_FIELD = 'Resent-From'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# resent-msg-id = "Resent-Message-ID:" msg-id CRLF
-
2
require 'mail/fields/common/common_message_id'
-
-
2
module Mail
-
2
class ResentMessageIdField < StructuredField
-
-
2
include CommonMessageId
-
-
2
FIELD_NAME = 'resent-message-id'
-
2
CAPITALIZED_FIELD = 'Resent-Message-ID'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self.parse
-
self
-
end
-
-
2
def name
-
'Resent-Message-ID'
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-Sender Field
-
#
-
# The Resent-Sender field inherits resent-sender StructuredField and handles the Resent-Sender: header
-
# field in the email.
-
#
-
# Sending resent_sender to a mail message will instantiate a Mail::Field object that
-
# has a ResentSenderField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-Sender field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_sender = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_sender #=> ['mikel@test.lindsaar.net']
-
# mail[:resent_sender] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentSenderField:0x180e1c4
-
# mail['resent-sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentSenderField:0x180e1c4
-
# mail['Resent-Sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentSenderField:0x180e1c4
-
#
-
# mail.resent_sender.to_s #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_sender.addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail.resent_sender.formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ResentSenderField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'resent-sender'
-
2
CAPITALIZED_FIELD = 'Resent-Sender'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def addresses
-
[address.address]
-
end
-
-
2
def address
-
address_list.addresses.first
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Resent-To Field
-
#
-
# The Resent-To field inherits resent-to StructuredField and handles the Resent-To: header
-
# field in the email.
-
#
-
# Sending resent_to to a mail message will instantiate a Mail::Field object that
-
# has a ResentToField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Resent-To field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.resent_to = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentToField:0x180e1c4
-
# mail['resent-to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentToField:0x180e1c4
-
# mail['Resent-To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ResentToField:0x180e1c4
-
#
-
# mail[:resent_to].encoded #=> 'Resent-To: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:resent_to].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:resent_to].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:resent_to].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ResentToField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'resent-to'
-
2
CAPITALIZED_FIELD = 'Resent-To'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# 4.4.3. REPLY-TO / RESENT-REPLY-TO
-
#
-
# Note: The "Return-Path" field is added by the mail transport
-
# service, at the time of final deliver. It is intended
-
# to identify a path back to the orginator of the mes-
-
# sage. The "Reply-To" field is added by the message
-
# originator and is intended to direct replies.
-
#
-
# trace = [return]
-
# 1*received
-
#
-
# return = "Return-Path:" path CRLF
-
#
-
# path = ([CFWS] "<" ([CFWS] / addr-spec) ">" [CFWS]) /
-
# obs-path
-
#
-
# received = "Received:" name-val-list ";" date-time CRLF
-
#
-
# name-val-list = [CFWS] [name-val-pair *(CFWS name-val-pair)]
-
#
-
# name-val-pair = item-name CFWS item-value
-
#
-
# item-name = ALPHA *(["-"] (ALPHA / DIGIT))
-
#
-
# item-value = 1*angle-addr / addr-spec /
-
# atom / domain / msg-id
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ReturnPathField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'return-path'
-
2
CAPITALIZED_FIELD = 'Return-Path'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
value = nil if value == '<>'
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
"#{CAPITALIZED_FIELD}: <#{address}>\r\n"
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
2
def address
-
addresses.first
-
end
-
-
2
def default
-
address
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = Sender Field
-
#
-
# The Sender field inherits sender StructuredField and handles the Sender: header
-
# field in the email.
-
#
-
# Sending sender to a mail message will instantiate a Mail::Field object that
-
# has a SenderField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one Sender field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.sender = 'Mikel Lindsaar <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
# mail[:sender] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::SenderField:0x180e1c4
-
# mail['sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::SenderField:0x180e1c4
-
# mail['Sender'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::SenderField:0x180e1c4
-
#
-
# mail[:sender].encoded #=> "Sender: Mikel Lindsaar <mikel@test.lindsaar.net>\r\n"
-
# mail[:sender].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>'
-
# mail[:sender].addresses #=> ['mikel@test.lindsaar.net']
-
# mail[:sender].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class SenderField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'sender'
-
2
CAPITALIZED_FIELD = 'Sender'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def addresses
-
[address.address]
-
end
-
-
2
def address
-
address_list.addresses.first
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
2
def default
-
address.address
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/fields/common/common_field'
-
-
2
module Mail
-
# Provides access to a structured header field
-
#
-
# ===Per RFC 2822:
-
# 2.2.2. Structured Header Field Bodies
-
#
-
# Some field bodies in this standard have specific syntactical
-
# structure more restrictive than the unstructured field bodies
-
# described above. These are referred to as "structured" field bodies.
-
# Structured field bodies are sequences of specific lexical tokens as
-
# described in sections 3 and 4 of this standard. Many of these tokens
-
# are allowed (according to their syntax) to be introduced or end with
-
# comments (as described in section 3.2.3) as well as the space (SP,
-
# ASCII value 32) and horizontal tab (HTAB, ASCII value 9) characters
-
# (together known as the white space characters, WSP), and those WSP
-
# characters are subject to header "folding" and "unfolding" as
-
# described in section 2.2.3. Semantic analysis of structured field
-
# bodies is given along with their syntax.
-
2
class StructuredField
-
-
2
include Mail::CommonField
-
2
include Mail::Utilities
-
-
2
def initialize(name = nil, value = nil, charset = nil)
-
self.name = name
-
self.value = value
-
self.charset = charset
-
self
-
end
-
-
2
def charset
-
@charset
-
end
-
-
2
def charset=(val)
-
@charset = val
-
end
-
-
2
def default
-
decoded
-
end
-
-
2
def errors
-
[]
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# subject = "Subject:" unstructured CRLF
-
2
module Mail
-
2
class SubjectField < UnstructuredField
-
-
2
FIELD_NAME = 'subject'
-
2
CAPITALIZED_FIELD = "Subject"
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
#
-
# = To Field
-
#
-
# The To field inherits to StructuredField and handles the To: header
-
# field in the email.
-
#
-
# Sending to to a mail message will instantiate a Mail::Field object that
-
# has a ToField as its field type. This includes all Mail::CommonAddress
-
# module instance metods.
-
#
-
# Only one To field can appear in a header, though it can have multiple
-
# addresses and groups of addresses.
-
#
-
# == Examples:
-
#
-
# mail = Mail.new
-
# mail.to = 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:to] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ToField:0x180e1c4
-
# mail['to'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ToField:0x180e1c4
-
# mail['To'] #=> '#<Mail::Field:0x180e5e8 @field=#<Mail::ToField:0x180e1c4
-
#
-
# mail[:to].encoded #=> 'To: Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net\r\n'
-
# mail[:to].decoded #=> 'Mikel Lindsaar <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail[:to].addresses #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
# mail[:to].formatted #=> ['Mikel Lindsaar <mikel@test.lindsaar.net>', 'ada@test.lindsaar.net']
-
#
-
2
require 'mail/fields/common/common_address'
-
-
2
module Mail
-
2
class ToField < StructuredField
-
-
2
include Mail::CommonAddress
-
-
2
FIELD_NAME = 'to'
-
2
CAPITALIZED_FIELD = 'To'
-
-
2
def initialize(value = nil, charset = 'utf-8')
-
self.charset = charset
-
super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
-
self
-
end
-
-
2
def encoded
-
do_encode(CAPITALIZED_FIELD)
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require 'mail/fields/common/common_field'
-
-
2
module Mail
-
# Provides access to an unstructured header field
-
#
-
# ===Per RFC 2822:
-
# 2.2.1. Unstructured Header Field Bodies
-
#
-
# Some field bodies in this standard are defined simply as
-
# "unstructured" (which is specified below as any US-ASCII characters,
-
# except for CR and LF) with no further restrictions. These are
-
# referred to as unstructured field bodies. Semantically, unstructured
-
# field bodies are simply to be treated as a single line of characters
-
# with no further processing (except for header "folding" and
-
# "unfolding" as described in section 2.2.3).
-
2
class UnstructuredField
-
-
2
include Mail::CommonField
-
2
include Mail::Utilities
-
-
2
attr_accessor :charset
-
2
attr_reader :errors
-
-
2
def initialize(name, value, charset = nil)
-
@errors = []
-
-
if value.is_a?(Array)
-
# Probably has arrived here from a failed parse of an AddressList Field
-
value = value.join(', ')
-
else
-
# Ensure we are dealing with a string
-
value = value.to_s
-
end
-
-
if charset
-
self.charset = charset
-
else
-
if value.respond_to?(:encoding)
-
self.charset = value.encoding
-
else
-
self.charset = $KCODE
-
end
-
end
-
self.name = name
-
self.value = value
-
self
-
end
-
-
2
def encoded
-
do_encode
-
end
-
-
2
def decoded
-
do_decode
-
end
-
-
2
def default
-
decoded
-
end
-
-
2
def parse # An unstructured field does not parse
-
self
-
end
-
-
2
private
-
-
2
def do_encode
-
value.nil? ? '' : "#{wrapped_value}\r\n"
-
end
-
-
2
def do_decode
-
Utilities.blank?(value) ? nil : Encodings.decode_encode(value, :decode)
-
end
-
-
# 2.2.3. Long Header Fields
-
#
-
# Each header field is logically a single line of characters comprising
-
# the field name, the colon, and the field body. For convenience
-
# however, and to deal with the 998/78 character limitations per line,
-
# the field body portion of a header field can be split into a multiple
-
# line representation; this is called "folding". The general rule is
-
# that wherever this standard allows for folding white space (not
-
# simply WSP characters), a CRLF may be inserted before any WSP. For
-
# example, the header field:
-
#
-
# Subject: This is a test
-
#
-
# can be represented as:
-
#
-
# Subject: This
-
# is a test
-
#
-
# Note: Though structured field bodies are defined in such a way that
-
# folding can take place between many of the lexical tokens (and even
-
# within some of the lexical tokens), folding SHOULD be limited to
-
# placing the CRLF at higher-level syntactic breaks. For instance, if
-
# a field body is defined as comma-separated values, it is recommended
-
# that folding occur after the comma separating the structured items in
-
# preference to other places where the field could be folded, even if
-
# it is allowed elsewhere.
-
2
def wrapped_value # :nodoc:
-
wrap_lines(name, fold("#{name}: ".length))
-
end
-
-
# 6.2. Display of 'encoded-word's
-
#
-
# When displaying a particular header field that contains multiple
-
# 'encoded-word's, any 'linear-white-space' that separates a pair of
-
# adjacent 'encoded-word's is ignored. (This is to allow the use of
-
# multiple 'encoded-word's to represent long strings of unencoded text,
-
# without having to separate 'encoded-word's where spaces occur in the
-
# unencoded text.)
-
2
def wrap_lines(name, folded_lines)
-
result = ["#{name}: #{folded_lines.shift}"]
-
result.concat(folded_lines)
-
result.join("\r\n\s")
-
end
-
-
2
def fold(prepend = 0) # :nodoc:
-
encoding = normalized_encoding
-
decoded_string = decoded.to_s
-
should_encode = decoded_string.not_ascii_only?
-
if should_encode
-
first = true
-
words = decoded_string.split(/[ \t]/).map do |word|
-
if first
-
first = !first
-
else
-
word = " #{word}"
-
end
-
if word.not_ascii_only?
-
word
-
else
-
word.scan(/.{7}|.+$/)
-
end
-
end.flatten
-
else
-
words = decoded_string.split(/[ \t]/)
-
end
-
-
folded_lines = []
-
while !words.empty?
-
limit = 78 - prepend
-
limit = limit - 7 - encoding.length if should_encode
-
line = String.new
-
first_word = true
-
while !words.empty?
-
break unless word = words.first.dup
-
word.encode!(charset) if charset && word.respond_to?(:encode!)
-
word = encode(word) if should_encode
-
word = encode_crlf(word)
-
# Skip to next line if we're going to go past the limit
-
# Unless this is the first word, in which case we're going to add it anyway
-
# Note: This means that a word that's longer than 998 characters is going to break the spec. Please fix if this is a problem for you.
-
# (The fix, it seems, would be to use encoded-word encoding on it, because that way you can break it across multiple lines and
-
# the linebreak will be ignored)
-
break if !line.empty? && (line.length + word.length + 1 > limit)
-
# Remove the word from the queue ...
-
words.shift
-
# Add word separator
-
if first_word
-
first_word = false
-
else
-
line << " " if !should_encode
-
end
-
-
# ... add it in encoded form to the current line
-
line << word
-
end
-
# Encode the line if necessary
-
line = "=?#{encoding}?Q?#{line}?=" if should_encode
-
# Add the line to the output and reset the prepend
-
folded_lines << line
-
prepend = 0
-
end
-
folded_lines
-
end
-
-
2
def encode(value)
-
value = [value].pack(CAPITAL_M).gsub(EQUAL_LF, EMPTY)
-
value.gsub!(/"/, '=22')
-
value.gsub!(/\(/, '=28')
-
value.gsub!(/\)/, '=29')
-
value.gsub!(/\?/, '=3F')
-
value.gsub!(/_/, '=5F')
-
value.gsub!(/ /, '_')
-
value
-
end
-
-
2
def encode_crlf(value)
-
value.gsub!(CR, CR_ENCODED)
-
value.gsub!(LF, LF_ENCODED)
-
value
-
end
-
-
2
def normalized_encoding
-
encoding = charset.to_s.upcase.gsub('_', '-')
-
encoding = 'UTF-8' if encoding == 'UTF8' # Ruby 1.8.x and $KCODE == 'u'
-
encoding
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
-
# Provides access to a header object.
-
#
-
# ===Per RFC2822
-
#
-
# 2.2. Header Fields
-
#
-
# Header fields are lines composed of a field name, followed by a colon
-
# (":"), followed by a field body, and terminated by CRLF. A field
-
# name MUST be composed of printable US-ASCII characters (i.e.,
-
# characters that have values between 33 and 126, inclusive), except
-
# colon. A field body may be composed of any US-ASCII characters,
-
# except for CR and LF. However, a field body may contain CRLF when
-
# used in header "folding" and "unfolding" as described in section
-
# 2.2.3. All field bodies MUST conform to the syntax described in
-
# sections 3 and 4 of this standard.
-
2
class Header
-
2
include Constants
-
2
include Utilities
-
2
include Enumerable
-
-
2
@@maximum_amount = 1000
-
-
# Large amount of headers in Email might create extra high CPU load
-
# Use this parameter to limit number of headers that will be parsed by
-
# mail library.
-
# Default: 1000
-
2
def self.maximum_amount
-
@@maximum_amount
-
end
-
-
2
def self.maximum_amount=(value)
-
@@maximum_amount = value
-
end
-
-
# Creates a new header object.
-
#
-
# Accepts raw text or nothing. If given raw text will attempt to parse
-
# it and split it into the various fields, instantiating each field as
-
# it goes.
-
#
-
# If it finds a field that should be a structured field (such as content
-
# type), but it fails to parse it, it will simply make it an unstructured
-
# field and leave it alone. This will mean that the data is preserved but
-
# no automatic processing of that field will happen. If you find one of
-
# these cases, please make a patch and send it in, or at the least, send
-
# me the example so we can fix it.
-
2
def initialize(header_text = nil, charset = nil)
-
@charset = charset
-
self.raw_source = ::Mail::Utilities.to_crlf(header_text).lstrip
-
split_header if header_text
-
end
-
-
2
def initialize_copy(original)
-
super
-
@fields = @fields.dup
-
end
-
-
# The preserved raw source of the header as you passed it in, untouched
-
# for your Regexing glory.
-
2
def raw_source
-
@raw_source
-
end
-
-
# Returns an array of all the fields in the header in order that they
-
# were read in.
-
2
def fields
-
@fields ||= FieldList.new
-
end
-
-
# 3.6. Field definitions
-
#
-
# It is important to note that the header fields are not guaranteed to
-
# be in a particular order. They may appear in any order, and they
-
# have been known to be reordered occasionally when transported over
-
# the Internet. However, for the purposes of this standard, header
-
# fields SHOULD NOT be reordered when a message is transported or
-
# transformed. More importantly, the trace header fields and resent
-
# header fields MUST NOT be reordered, and SHOULD be kept in blocks
-
# prepended to the message. See sections 3.6.6 and 3.6.7 for more
-
# information.
-
#
-
# Populates the fields container with Field objects in the order it
-
# receives them in.
-
#
-
# Acceps an array of field string values, for example:
-
#
-
# h = Header.new
-
# h.fields = ['From: mikel@me.com', 'To: bob@you.com']
-
2
def fields=(unfolded_fields)
-
@fields = Mail::FieldList.new
-
warn "Warning: more than #{self.class.maximum_amount} header fields only using the first #{self.class.maximum_amount}" if unfolded_fields.length > self.class.maximum_amount
-
unfolded_fields[0..(self.class.maximum_amount-1)].each do |field|
-
-
field = Field.new(field, nil, charset)
-
if limited_field?(field.name) && (selected = select_field_for(field.name)) && selected.any?
-
selected.first.update(field.name, field.value)
-
else
-
@fields << field
-
end
-
end
-
-
end
-
-
2
def errors
-
@fields.map(&:errors).flatten(1)
-
end
-
-
# 3.6. Field definitions
-
#
-
# The following table indicates limits on the number of times each
-
# field may occur in a message header as well as any special
-
# limitations on the use of those fields. An asterisk next to a value
-
# in the minimum or maximum column indicates that a special restriction
-
# appears in the Notes column.
-
#
-
# <snip table from 3.6>
-
#
-
# As per RFC, many fields can appear more than once, we will return a string
-
# of the value if there is only one header, or if there is more than one
-
# matching header, will return an array of values in order that they appear
-
# in the header ordered from top to bottom.
-
#
-
# Example:
-
#
-
# h = Header.new
-
# h.fields = ['To: mikel@me.com', 'X-Mail-SPAM: 15', 'X-Mail-SPAM: 20']
-
# h['To'] #=> 'mikel@me.com'
-
# h['X-Mail-SPAM'] #=> ['15', '20']
-
2
def [](name)
-
name = dasherize(name)
-
name.downcase!
-
selected = select_field_for(name)
-
case
-
when selected.length > 1
-
selected.map { |f| f }
-
when !Utilities.blank?(selected)
-
selected.first
-
else
-
nil
-
end
-
end
-
-
# Sets the FIRST matching field in the header to passed value, or deletes
-
# the FIRST field matched from the header if passed nil
-
#
-
# Example:
-
#
-
# h = Header.new
-
# h.fields = ['To: mikel@me.com', 'X-Mail-SPAM: 15', 'X-Mail-SPAM: 20']
-
# h['To'] = 'bob@you.com'
-
# h['To'] #=> 'bob@you.com'
-
# h['X-Mail-SPAM'] = '10000'
-
# h['X-Mail-SPAM'] # => ['15', '20', '10000']
-
# h['X-Mail-SPAM'] = nil
-
# h['X-Mail-SPAM'] # => nil
-
2
def []=(name, value)
-
name = dasherize(name)
-
if name.include?(':')
-
raise ArgumentError, "Header names may not contain a colon: #{name.inspect}"
-
end
-
fn = name.downcase
-
selected = select_field_for(fn)
-
-
case
-
# User wants to delete the field
-
when !Utilities.blank?(selected) && value == nil
-
fields.delete_if { |f| selected.include?(f) }
-
-
# User wants to change the field
-
when !Utilities.blank?(selected) && limited_field?(fn)
-
selected.first.update(fn, value)
-
-
# User wants to create the field
-
else
-
# Need to insert in correct order for trace fields
-
self.fields << Field.new(name.to_s, value, charset)
-
end
-
if dasherize(fn) == "content-type"
-
# Update charset if specified in Content-Type
-
params = self[:content_type].parameters rescue nil
-
@charset = params[:charset] if params && params[:charset]
-
end
-
end
-
-
2
def charset
-
@charset
-
end
-
-
2
def charset=(val)
-
params = self[:content_type].parameters rescue nil
-
if params
-
params[:charset] = val
-
end
-
@charset = val
-
end
-
-
2
LIMITED_FIELDS = %w[ date from sender reply-to to cc bcc
-
message-id in-reply-to references subject
-
return-path content-type mime-version
-
content-transfer-encoding content-description
-
content-id content-disposition content-location]
-
-
2
def encoded
-
buffer = String.new
-
buffer.force_encoding('us-ascii') if buffer.respond_to?(:force_encoding)
-
fields.each do |field|
-
buffer << field.encoded
-
end
-
buffer
-
end
-
-
2
def to_s
-
encoded
-
end
-
-
2
def decoded
-
raise NoMethodError, 'Can not decode an entire header as there could be character set conflicts, try calling #decoded on the various fields.'
-
end
-
-
2
def field_summary
-
fields.map { |f| "<#{f.name}: #{f.value}>" }.join(", ")
-
end
-
-
# Returns true if the header has a Message-ID defined (empty or not)
-
2
def has_message_id?
-
!fields.select { |f| f.responsible_for?('Message-ID') }.empty?
-
end
-
-
# Returns true if the header has a Content-ID defined (empty or not)
-
2
def has_content_id?
-
!fields.select { |f| f.responsible_for?('Content-ID') }.empty?
-
end
-
-
# Returns true if the header has a Date defined (empty or not)
-
2
def has_date?
-
!fields.select { |f| f.responsible_for?('Date') }.empty?
-
end
-
-
# Returns true if the header has a MIME version defined (empty or not)
-
2
def has_mime_version?
-
!fields.select { |f| f.responsible_for?('Mime-Version') }.empty?
-
end
-
-
2
private
-
-
2
def raw_source=(val)
-
@raw_source = val
-
end
-
-
# Splits an unfolded and line break cleaned header into individual field
-
# strings.
-
2
def split_header
-
self.fields = raw_source.split(HEADER_SPLIT)
-
end
-
-
2
def select_field_for(name)
-
fields.select { |f| f.responsible_for?(name) }
-
end
-
-
2
def limited_field?(name)
-
LIMITED_FIELDS.include?(name.to_s.downcase)
-
end
-
-
# Enumerable support; yield each field in order to the block if there is one,
-
# or return an Enumerator for them if there isn't.
-
2
def each( &block )
-
return self.fields.each( &block ) if block
-
self.fields.each
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
# This is an almost cut and paste from ActiveSupport v3.0.6, copied in here so that Mail
-
# itself does not depend on ActiveSupport to avoid versioning conflicts
-
-
2
module Mail
-
2
class IndifferentHash < Hash
-
-
2
def initialize(constructor = {})
-
if constructor.is_a?(Hash)
-
super()
-
update(constructor)
-
else
-
super(constructor)
-
end
-
end
-
-
2
def default(key = nil)
-
if key.is_a?(Symbol) && include?(key = key.to_s)
-
self[key]
-
else
-
super
-
end
-
end
-
-
2
def self.new_from_hash_copying_default(hash)
-
IndifferentHash.new(hash).tap do |new_hash|
-
new_hash.default = hash.default
-
end
-
end
-
-
2
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
-
2
alias_method :regular_update, :update unless method_defined?(:regular_update)
-
-
# Assigns a new value to the hash:
-
#
-
# hash = HashWithIndifferentAccess.new
-
# hash[:key] = "value"
-
#
-
2
def []=(key, value)
-
regular_writer(convert_key(key), convert_value(value))
-
end
-
-
2
alias_method :store, :[]=
-
-
# Updates the instantized hash with values from the second:
-
#
-
# hash_1 = HashWithIndifferentAccess.new
-
# hash_1[:key] = "value"
-
#
-
# hash_2 = HashWithIndifferentAccess.new
-
# hash_2[:key] = "New Value!"
-
#
-
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
-
#
-
2
def update(other_hash)
-
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
-
self
-
end
-
-
2
alias_method :merge!, :update
-
-
# Checks the hash for a key matching the argument passed in:
-
#
-
# hash = HashWithIndifferentAccess.new
-
# hash["key"] = "value"
-
# hash.key? :key # => true
-
# hash.key? "key" # => true
-
#
-
2
def key?(key)
-
super(convert_key(key))
-
end
-
-
2
alias_method :include?, :key?
-
2
alias_method :has_key?, :key?
-
2
alias_method :member?, :key?
-
-
# Fetches the value for the specified key, same as doing hash[key]
-
2
def fetch(key, *extras)
-
super(convert_key(key), *extras)
-
end
-
-
# Returns an array of the values at the specified indices:
-
#
-
# hash = HashWithIndifferentAccess.new
-
# hash[:a] = "x"
-
# hash[:b] = "y"
-
# hash.values_at("a", "b") # => ["x", "y"]
-
#
-
2
def values_at(*indices)
-
indices.collect {|key| self[convert_key(key)]}
-
end
-
-
# Returns an exact copy of the hash.
-
2
def dup
-
IndifferentHash.new(self)
-
end
-
-
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
-
# Does not overwrite the existing hash.
-
2
def merge(hash)
-
self.dup.update(hash)
-
end
-
-
# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
-
# This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
-
2
def reverse_merge(other_hash)
-
super self.class.new_from_hash_copying_default(other_hash)
-
end
-
-
2
def reverse_merge!(other_hash)
-
replace(reverse_merge( other_hash ))
-
end
-
-
# Removes a specified key from the hash.
-
2
def delete(key)
-
super(convert_key(key))
-
end
-
-
2
def stringify_keys!; self end
-
2
def stringify_keys; dup end
-
2
def symbolize_keys; to_hash.symbolize_keys end
-
2
def to_options!; self end
-
-
2
def to_hash
-
Hash.new(default).merge!(self)
-
end
-
-
2
protected
-
-
2
def convert_key(key)
-
key.kind_of?(Symbol) ? key.to_s : key
-
end
-
-
2
def convert_value(value)
-
if value.class == Hash
-
self.class.new_from_hash_copying_default(value)
-
elsif value.is_a?(Array)
-
value.dup.replace(value.map { |e| convert_value(e) })
-
else
-
value
-
end
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
-
# Allows you to create a new Mail::Message object.
-
#
-
# You can make an email via passing a string or passing a block.
-
#
-
# For example, the following two examples will create the same email
-
# message:
-
#
-
# Creating via a string:
-
#
-
# string = "To: mikel@test.lindsaar.net\r\n"
-
# string << "From: bob@test.lindsaar.net\r\n"
-
# string << "Subject: This is an email\r\n"
-
# string << "\r\n"
-
# string << "This is the body"
-
# Mail.new(string)
-
#
-
# Or creating via a block:
-
#
-
# message = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'bob@test.lindsaar.net'
-
# subject 'This is an email'
-
# body 'This is the body'
-
# end
-
#
-
# Or creating via a hash (or hash like object):
-
#
-
# message = Mail.new({:to => 'mikel@test.lindsaar.net',
-
# 'from' => 'bob@test.lindsaar.net',
-
# :subject => 'This is an email',
-
# :body => 'This is the body' })
-
#
-
# Note, the hash keys can be strings or symbols, the passed in object
-
# does not need to be a hash, it just needs to respond to :each_pair
-
# and yield each key value pair.
-
#
-
# As a side note, you can also create a new email through creating
-
# a Mail::Message object directly and then passing in values via string,
-
# symbol or direct method calls. See Mail::Message for more information.
-
#
-
# mail = Mail.new
-
# mail.to = 'mikel@test.lindsaar.net'
-
# mail[:from] = 'bob@test.lindsaar.net'
-
# mail['subject'] = 'This is an email'
-
# mail.body = 'This is the body'
-
2
def self.new(*args, &block)
-
Message.new(args, &block)
-
end
-
-
# Sets the default delivery method and retriever method for all new Mail objects.
-
# The delivery_method and retriever_method default to :smtp and :pop3, with defaults
-
# set.
-
#
-
# So sending a new email, if you have an SMTP server running on localhost is
-
# as easy as:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'bob@test.lindsaar.net'
-
# subject 'hi there!'
-
# body 'this is a body'
-
# end
-
#
-
# If you do not specify anything, you will get the following equivalent code set in
-
# every new mail object:
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "localhost",
-
# :port => 25,
-
# :domain => 'localhost.localdomain',
-
# :user_name => nil,
-
# :password => nil,
-
# :authentication => nil,
-
# :enable_starttls_auto => true }
-
#
-
# retriever_method :pop3, { :address => "localhost",
-
# :port => 995,
-
# :user_name => nil,
-
# :password => nil,
-
# :enable_ssl => true }
-
# end
-
#
-
# Mail.delivery_method.new #=> Mail::SMTP instance
-
# Mail.retriever_method.new #=> Mail::POP3 instance
-
#
-
# Each mail object inherits the default set in Mail.delivery_method, however, on
-
# a per email basis, you can override the method:
-
#
-
# mail.delivery_method :sendmail
-
#
-
# Or you can override the method and pass in settings:
-
#
-
# mail.delivery_method :sendmail, { :address => 'some.host' }
-
#
-
# You can also just modify the settings:
-
#
-
# mail.delivery_settings = { :address => 'some.host' }
-
#
-
# The passed in hash is just merged against the defaults with +merge!+ and the result
-
# assigned the mail object. So the above example will change only the :address value
-
# of the global smtp_settings to be 'some.host', keeping all other values
-
2
def self.defaults(&block)
-
Configuration.instance.instance_eval(&block)
-
end
-
-
# Returns the delivery method selected, defaults to an instance of Mail::SMTP
-
2
def self.delivery_method
-
Configuration.instance.delivery_method
-
end
-
-
# Returns the retriever method selected, defaults to an instance of Mail::POP3
-
2
def self.retriever_method
-
Configuration.instance.retriever_method
-
end
-
-
# Send an email using the default configuration. You do need to set a default
-
# configuration first before you use self.deliver, if you don't, an appropriate
-
# error will be raised telling you to.
-
#
-
# If you do not specify a delivery type, SMTP will be used.
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'This is a test email'
-
# body 'Not much to say here'
-
# end
-
#
-
# You can also do:
-
#
-
# mail = Mail.read('email.eml')
-
# mail.deliver!
-
#
-
# And your email object will be created and sent.
-
2
def self.deliver(*args, &block)
-
mail = self.new(args, &block)
-
mail.deliver
-
mail
-
end
-
-
# Find emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
2
def self.find(*args, &block)
-
retriever_method.find(*args, &block)
-
end
-
-
# Finds and then deletes retrieved emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
2
def self.find_and_delete(*args, &block)
-
retriever_method.find_and_delete(*args, &block)
-
end
-
-
# Receive the first email(s) from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
2
def self.first(*args, &block)
-
retriever_method.first(*args, &block)
-
end
-
-
# Receive the first email(s) from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
2
def self.last(*args, &block)
-
retriever_method.last(*args, &block)
-
end
-
-
# Receive all emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
2
def self.all(*args, &block)
-
retriever_method.all(*args, &block)
-
end
-
-
# Reads in an email message from a path and instantiates it as a new Mail::Message
-
2
def self.read(filename)
-
self.new(File.open(filename, 'rb') { |f| f.read })
-
end
-
-
# Delete all emails from the default retriever
-
# See Mail::Retriever for a complete documentation.
-
2
def self.delete_all(*args, &block)
-
retriever_method.delete_all(*args, &block)
-
end
-
-
# Instantiates a new Mail::Message using a string
-
2
def Mail.read_from_string(mail_as_string)
-
Mail.new(mail_as_string)
-
end
-
-
2
def Mail.connection(&block)
-
retriever_method.connection(&block)
-
end
-
-
# Initialize the observers and interceptors arrays
-
2
@@delivery_notification_observers = []
-
2
@@delivery_interceptors = []
-
-
# You can register an object to be informed of every email that is sent through
-
# this method.
-
#
-
# Your object needs to respond to a single method #delivered_email(mail)
-
# which receives the email that is sent.
-
2
def self.register_observer(observer)
-
unless @@delivery_notification_observers.include?(observer)
-
@@delivery_notification_observers << observer
-
end
-
end
-
-
# Unregister the given observer, allowing mail to resume operations
-
# without it.
-
2
def self.unregister_observer(observer)
-
@@delivery_notification_observers.delete(observer)
-
end
-
-
# You can register an object to be given every mail object that will be sent,
-
# before it is sent. So if you want to add special headers or modify any
-
# email that gets sent through the Mail library, you can do so.
-
#
-
# Your object needs to respond to a single method #delivering_email(mail)
-
# which receives the email that is about to be sent. Make your modifications
-
# directly to this object.
-
2
def self.register_interceptor(interceptor)
-
unless @@delivery_interceptors.include?(interceptor)
-
@@delivery_interceptors << interceptor
-
end
-
end
-
-
# Unregister the given interceptor, allowing mail to resume operations
-
# without it.
-
2
def self.unregister_interceptor(interceptor)
-
@@delivery_interceptors.delete(interceptor)
-
end
-
-
2
def self.inform_observers(mail)
-
@@delivery_notification_observers.each do |observer|
-
observer.delivered_email(mail)
-
end
-
end
-
-
2
def self.inform_interceptors(mail)
-
@@delivery_interceptors.each do |interceptor|
-
interceptor.delivering_email(mail)
-
end
-
end
-
-
2
protected
-
-
2
RANDOM_TAG='%x%x_%x%x%d%x'
-
-
2
def self.random_tag
-
t = Time.now
-
sprintf(RANDOM_TAG,
-
t.to_i, t.tv_usec,
-
$$, Thread.current.object_id.abs, self.uniq, rand(255))
-
end
-
-
2
private
-
-
2
def self.something_random
-
2
(Thread.current.object_id * rand(255) / Time.now.to_f).to_s.slice(-3..-1).to_i
-
end
-
-
2
def self.uniq
-
@@uniq += 1
-
end
-
-
2
@@uniq = self.something_random
-
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
module Matchers
-
2
def any_attachment
-
AnyAttachmentMatcher.new
-
end
-
-
2
def an_attachment_with_filename(filename)
-
AttachmentFilenameMatcher.new(filename)
-
end
-
-
2
class AnyAttachmentMatcher
-
2
def ===(other)
-
other.attachment?
-
end
-
end
-
-
2
class AttachmentFilenameMatcher
-
2
attr_reader :filename
-
2
def initialize(filename)
-
@filename = filename
-
end
-
-
2
def ===(other)
-
other.attachment? && other.filename == filename
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
module Matchers
-
2
def have_sent_email
-
HasSentEmailMatcher.new(self)
-
end
-
-
2
class HasSentEmailMatcher
-
2
def initialize(_context)
-
end
-
-
2
def matches?(subject)
-
matching_deliveries = filter_matched_deliveries(Mail::TestMailer.deliveries)
-
!(matching_deliveries.empty?)
-
end
-
-
2
def from(sender)
-
@sender = sender
-
self
-
end
-
-
2
def to(recipient_or_list)
-
@recipients ||= []
-
-
if recipient_or_list.kind_of?(Array)
-
@recipients += recipient_or_list
-
else
-
@recipients << recipient_or_list
-
end
-
self
-
end
-
-
2
def cc(recipient_or_list)
-
@copy_recipients ||= []
-
-
if recipient_or_list.kind_of?(Array)
-
@copy_recipients += recipient_or_list
-
else
-
@copy_recipients << recipient_or_list
-
end
-
self
-
end
-
-
2
def bcc(recipient_or_list)
-
@blind_copy_recipients ||= []
-
@blind_copy_recipients.concat(Array(recipient_or_list))
-
self
-
end
-
-
2
def with_attachments(attachments)
-
@attachments ||= []
-
@attachments.concat(Array(attachments))
-
self
-
end
-
-
2
def with_no_attachments
-
@having_attachments = false
-
self
-
end
-
-
2
def with_any_attachments
-
@having_attachments = true
-
self
-
end
-
-
2
def with_subject(subject)
-
@subject = subject
-
self
-
end
-
-
2
def matching_subject(subject_matcher)
-
@subject_matcher = subject_matcher
-
self
-
end
-
-
2
def with_body(body)
-
@body = body
-
self
-
end
-
-
2
def matching_body(body_matcher)
-
@body_matcher = body_matcher
-
self
-
end
-
-
2
def description
-
result = "send a matching email"
-
result
-
end
-
-
2
def failure_message
-
result = "Expected email to be sent "
-
result += explain_expectations
-
result += dump_deliveries
-
result
-
end
-
-
2
def failure_message_when_negated
-
result = "Expected no email to be sent "
-
result += explain_expectations
-
result += dump_deliveries
-
result
-
end
-
-
2
protected
-
-
2
def filter_matched_deliveries(deliveries)
-
candidate_deliveries = deliveries
-
modifiers =
-
%w(sender recipients copy_recipients blind_copy_recipients subject
-
subject_matcher body body_matcher having_attachments attachments)
-
modifiers.each do |modifier_name|
-
next unless instance_variable_defined?("@#{modifier_name}")
-
candidate_deliveries = candidate_deliveries.select{|matching_delivery| self.send("matches_on_#{modifier_name}?", matching_delivery)}
-
end
-
-
candidate_deliveries
-
end
-
-
2
def matches_on_sender?(delivery)
-
delivery.from.include?(@sender)
-
end
-
-
2
def matches_on_recipients?(delivery)
-
@recipients.all? {|recipient| delivery.to.include?(recipient) }
-
end
-
-
2
def matches_on_copy_recipients?(delivery)
-
@copy_recipients.all? {|recipient| delivery.cc.include?(recipient) }
-
end
-
-
2
def matches_on_blind_copy_recipients?(delivery)
-
@blind_copy_recipients.all? {|recipient| delivery.bcc.include?(recipient) }
-
end
-
-
2
def matches_on_subject?(delivery)
-
delivery.subject == @subject
-
end
-
-
2
def matches_on_subject_matcher?(delivery)
-
@subject_matcher.match delivery.subject
-
end
-
-
2
def matches_on_having_attachments?(delivery)
-
@having_attachments && delivery.attachments.any? ||
-
(!@having_attachments && delivery.attachments.none?)
-
end
-
-
2
def matches_on_attachments?(delivery)
-
@attachments.each_with_index.inject( true ) do |sent_attachments, (attachment, index)|
-
sent_attachments &&= (attachment === delivery.attachments[index])
-
end
-
end
-
-
2
def matches_on_body?(delivery)
-
delivery.body == @body
-
end
-
-
2
def matches_on_body_matcher?(delivery)
-
@body_matcher.match delivery.body.raw_source
-
end
-
-
2
def explain_expectations
-
result = ''
-
result += "from #{@sender} " if instance_variable_defined?('@sender')
-
result += "to #{@recipients.inspect} " if instance_variable_defined?('@recipients')
-
result += "cc #{@copy_recipients.inspect} " if instance_variable_defined?('@copy_recipients')
-
result += "bcc #{@blind_copy_recipients.inspect} " if instance_variable_defined?('@blind_copy_recipients')
-
result += "with subject \"#{@subject}\" " if instance_variable_defined?('@subject')
-
result += "with subject matching \"#{@subject_matcher}\" " if instance_variable_defined?('@subject_matcher')
-
result += "with body \"#{@body}\" " if instance_variable_defined?('@body')
-
result += "with body matching \"#{@body_matcher}\" " if instance_variable_defined?('@body_matcher')
-
result
-
end
-
-
2
def dump_deliveries
-
"(actual deliveries: " + Mail::TestMailer.deliveries.inspect + ")"
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
require "yaml"
-
-
2
module Mail
-
# The Message class provides a single point of access to all things to do with an
-
# email message.
-
#
-
# You create a new email message by calling the Mail::Message.new method, or just
-
# Mail.new
-
#
-
# A Message object by default has the following objects inside it:
-
#
-
# * A Header object which contains all information and settings of the header of the email
-
# * Body object which contains all parts of the email that are not part of the header, this
-
# includes any attachments, body text, MIME parts etc.
-
#
-
# ==Per RFC2822
-
#
-
# 2.1. General Description
-
#
-
# At the most basic level, a message is a series of characters. A
-
# message that is conformant with this standard is comprised of
-
# characters with values in the range 1 through 127 and interpreted as
-
# US-ASCII characters [ASCII]. For brevity, this document sometimes
-
# refers to this range of characters as simply "US-ASCII characters".
-
#
-
# Note: This standard specifies that messages are made up of characters
-
# in the US-ASCII range of 1 through 127. There are other documents,
-
# specifically the MIME document series [RFC2045, RFC2046, RFC2047,
-
# RFC2048, RFC2049], that extend this standard to allow for values
-
# outside of that range. Discussion of those mechanisms is not within
-
# the scope of this standard.
-
#
-
# Messages are divided into lines of characters. A line is a series of
-
# characters that is delimited with the two characters carriage-return
-
# and line-feed; that is, the carriage return (CR) character (ASCII
-
# value 13) followed immediately by the line feed (LF) character (ASCII
-
# value 10). (The carriage-return/line-feed pair is usually written in
-
# this document as "CRLF".)
-
#
-
# A message consists of header fields (collectively called "the header
-
# of the message") followed, optionally, by a body. The header is a
-
# sequence of lines of characters with special syntax as defined in
-
# this standard. The body is simply a sequence of characters that
-
# follows the header and is separated from the header by an empty line
-
# (i.e., a line with nothing preceding the CRLF).
-
2
class Message
-
-
2
include Constants
-
2
include Utilities
-
-
# ==Making an email
-
#
-
# You can make an new mail object via a block, passing a string, file or direct assignment.
-
#
-
# ===Making an email via a block
-
#
-
# mail = Mail.new do
-
# from 'mikel@test.lindsaar.net'
-
# to 'you@test.lindsaar.net'
-
# subject 'This is a test email'
-
# body File.read('body.txt')
-
# end
-
#
-
# mail.to_s #=> "From: mikel@test.lindsaar.net\r\nTo: you@...
-
#
-
# ===Making an email via passing a string
-
#
-
# mail = Mail.new("To: mikel@test.lindsaar.net\r\nSubject: Hello\r\n\r\nHi there!")
-
# mail.body.to_s #=> 'Hi there!'
-
# mail.subject #=> 'Hello'
-
# mail.to #=> 'mikel@test.lindsaar.net'
-
#
-
# ===Making an email from a file
-
#
-
# mail = Mail.read('path/to/file.eml')
-
# mail.body.to_s #=> 'Hi there!'
-
# mail.subject #=> 'Hello'
-
# mail.to #=> 'mikel@test.lindsaar.net'
-
#
-
# ===Making an email via assignment
-
#
-
# You can assign values to a mail object via four approaches:
-
#
-
# * Message#field_name=(value)
-
# * Message#field_name(value)
-
# * Message#['field_name']=(value)
-
# * Message#[:field_name]=(value)
-
#
-
# Examples:
-
#
-
# mail = Mail.new
-
# mail['from'] = 'mikel@test.lindsaar.net'
-
# mail[:to] = 'you@test.lindsaar.net'
-
# mail.subject 'This is a test email'
-
# mail.body = 'This is a body'
-
#
-
# mail.to_s #=> "From: mikel@test.lindsaar.net\r\nTo: you@...
-
#
-
2
def initialize(*args, &block)
-
@body = nil
-
@body_raw = nil
-
@separate_parts = false
-
@text_part = nil
-
@html_part = nil
-
@errors = nil
-
@header = nil
-
@charset = self.class.default_charset
-
@defaulted_charset = true
-
-
@smtp_envelope_from = nil
-
@smtp_envelope_to = nil
-
-
@perform_deliveries = true
-
@raise_delivery_errors = true
-
-
@delivery_handler = nil
-
-
@delivery_method = Mail.delivery_method.dup
-
-
@transport_encoding = Mail::Encodings.get_encoding('7bit')
-
-
@mark_for_delete = false
-
-
if args.flatten.first.respond_to?(:each_pair)
-
init_with_hash(args.flatten.first)
-
else
-
init_with_string(args.flatten[0].to_s)
-
end
-
-
if block_given?
-
instance_eval(&block)
-
end
-
-
self
-
end
-
-
# If you assign a delivery handler, mail will call :deliver_mail on the
-
# object you assign to delivery_handler, it will pass itself as the
-
# single argument.
-
#
-
# If you define a delivery_handler, then you are responsible for the
-
# following actions in the delivery cycle:
-
#
-
# * Appending the mail object to Mail.deliveries as you see fit.
-
# * Checking the mail.perform_deliveries flag to decide if you should
-
# actually call :deliver! the mail object or not.
-
# * Checking the mail.raise_delivery_errors flag to decide if you
-
# should raise delivery errors if they occur.
-
# * Actually calling :deliver! (with the bang) on the mail object to
-
# get it to deliver itself.
-
#
-
# A simplest implementation of a delivery_handler would be
-
#
-
# class MyObject
-
#
-
# def initialize
-
# @mail = Mail.new('To: mikel@test.lindsaar.net')
-
# @mail.delivery_handler = self
-
# end
-
#
-
# attr_accessor :mail
-
#
-
# def deliver_mail(mail)
-
# yield
-
# end
-
# end
-
#
-
# Then doing:
-
#
-
# obj = MyObject.new
-
# obj.mail.deliver
-
#
-
# Would cause Mail to call obj.deliver_mail passing itself as a parameter,
-
# which then can just yield and let Mail do its own private do_delivery
-
# method.
-
2
attr_accessor :delivery_handler
-
-
# If set to false, mail will go through the motions of doing a delivery,
-
# but not actually call the delivery method or append the mail object to
-
# the Mail.deliveries collection. Useful for testing.
-
#
-
# Mail.deliveries.size #=> 0
-
# mail.delivery_method :smtp
-
# mail.perform_deliveries = false
-
# mail.deliver # Mail::SMTP not called here
-
# Mail.deliveries.size #=> 0
-
#
-
# If you want to test and query the Mail.deliveries collection to see what
-
# mail you sent, you should set perform_deliveries to true and use
-
# the :test mail delivery_method:
-
#
-
# Mail.deliveries.size #=> 0
-
# mail.delivery_method :test
-
# mail.perform_deliveries = true
-
# mail.deliver
-
# Mail.deliveries.size #=> 1
-
#
-
# This setting is ignored by mail (though still available as a flag) if you
-
# define a delivery_handler
-
2
attr_accessor :perform_deliveries
-
-
# If set to false, mail will silently catch and ignore any exceptions
-
# raised through attempting to deliver an email.
-
#
-
# This setting is ignored by mail (though still available as a flag) if you
-
# define a delivery_handler
-
2
attr_accessor :raise_delivery_errors
-
-
2
def self.default_charset; @@default_charset; end
-
4
def self.default_charset=(charset); @@default_charset = charset; end
-
2
self.default_charset = 'UTF-8'
-
-
2
def register_for_delivery_notification(observer)
-
STDERR.puts("Message#register_for_delivery_notification is deprecated, please call Mail.register_observer instead")
-
Mail.register_observer(observer)
-
end
-
-
2
def inform_observers
-
Mail.inform_observers(self)
-
end
-
-
2
def inform_interceptors
-
Mail.inform_interceptors(self)
-
end
-
-
# Delivers an mail object.
-
#
-
# Examples:
-
#
-
# mail = Mail.read('file.eml')
-
# mail.deliver
-
2
def deliver
-
inform_interceptors
-
if delivery_handler
-
delivery_handler.deliver_mail(self) { do_delivery }
-
else
-
do_delivery
-
end
-
inform_observers
-
self
-
end
-
-
# This method bypasses checking perform_deliveries and raise_delivery_errors,
-
# so use with caution.
-
#
-
# It still however fires off the interceptors and calls the observers callbacks if they are defined.
-
#
-
# Returns self
-
2
def deliver!
-
inform_interceptors
-
response = delivery_method.deliver!(self)
-
inform_observers
-
delivery_method.settings[:return_response] ? response : self
-
end
-
-
2
def delivery_method(method = nil, settings = {})
-
unless method
-
@delivery_method
-
else
-
@delivery_method = Configuration.instance.lookup_delivery_method(method).new(settings)
-
end
-
end
-
-
2
def reply(*args, &block)
-
self.class.new.tap do |reply|
-
if message_id
-
bracketed_message_id = "<#{message_id}>"
-
reply.in_reply_to = bracketed_message_id
-
if !references.nil?
-
refs = [references].flatten.map { |r| "<#{r}>" }
-
refs << bracketed_message_id
-
reply.references = refs.join(' ')
-
elsif !in_reply_to.nil? && !in_reply_to.kind_of?(Array)
-
reply.references = "<#{in_reply_to}> #{bracketed_message_id}"
-
end
-
reply.references ||= bracketed_message_id
-
end
-
if subject
-
reply.subject = subject =~ /^Re:/i ? subject : "RE: #{subject}"
-
end
-
if reply_to || from
-
reply.to = self[reply_to ? :reply_to : :from].to_s
-
end
-
if to
-
reply.from = self[:to].formatted.first.to_s
-
end
-
-
unless args.empty?
-
if args.flatten.first.respond_to?(:each_pair)
-
reply.send(:init_with_hash, args.flatten.first)
-
else
-
reply.send(:init_with_string, args.flatten[0].to_s.strip)
-
end
-
end
-
-
if block_given?
-
reply.instance_eval(&block)
-
end
-
end
-
end
-
-
# Provides the operator needed for sort et al.
-
#
-
# Compares this mail object with another mail object, this is done by date, so an
-
# email that is older than another will appear first.
-
#
-
# Example:
-
#
-
# mail1 = Mail.new do
-
# date(Time.now)
-
# end
-
# mail2 = Mail.new do
-
# date(Time.now - 86400) # 1 day older
-
# end
-
# [mail2, mail1].sort #=> [mail2, mail1]
-
2
def <=>(other)
-
if other.nil?
-
1
-
else
-
self.date <=> other.date
-
end
-
end
-
-
# Two emails are the same if they have the same fields and body contents. One
-
# gotcha here is that Mail will insert Message-IDs when calling encoded, so doing
-
# mail1.encoded == mail2.encoded is most probably not going to return what you think
-
# as the assigned Message-IDs by Mail (if not already defined as the same) will ensure
-
# that the two objects are unique, and this comparison will ALWAYS return false.
-
#
-
# So the == operator has been defined like so: Two messages are the same if they have
-
# the same content, ignoring the Message-ID field, unless BOTH emails have a defined and
-
# different Message-ID value, then they are false.
-
#
-
# So, in practice the == operator works like this:
-
#
-
# m1 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Subject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> true
-
#
-
# m1 = Mail.new("Message-ID: <1234@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m2 = Mail.new("Message-ID: <DIFFERENT@test>\r\nSubject: Hello\r\n\r\nHello")
-
# m1 == m2 #=> false
-
2
def ==(other)
-
return false unless other.respond_to?(:encoded)
-
-
if self.message_id && other.message_id
-
self.encoded == other.encoded
-
else
-
self_message_id, other_message_id = self.message_id, other.message_id
-
begin
-
self.message_id, other.message_id = '<temp@test>', '<temp@test>'
-
self.encoded == other.encoded
-
ensure
-
self.message_id, other.message_id = self_message_id, other_message_id
-
end
-
end
-
end
-
-
2
def initialize_copy(original)
-
super
-
@header = @header.dup
-
end
-
-
# Provides access to the raw source of the message as it was when it
-
# was instantiated. This is set at initialization and so is untouched
-
# by the parsers or decoder / encoders
-
#
-
# Example:
-
#
-
# mail = Mail.new('This is an invalid email message')
-
# mail.raw_source #=> "This is an invalid email message"
-
2
def raw_source
-
@raw_source
-
end
-
-
# Sets the envelope from for the email
-
2
def set_envelope( val )
-
@raw_envelope = val
-
@envelope = Mail::Envelope.new( val )
-
end
-
-
# The raw_envelope is the From mikel@test.lindsaar.net Mon May 2 16:07:05 2009
-
# type field that you can see at the top of any email that has come
-
# from a mailbox
-
2
def raw_envelope
-
@raw_envelope
-
end
-
-
2
def envelope_from
-
@envelope ? @envelope.from : nil
-
end
-
-
2
def envelope_date
-
@envelope ? @envelope.date : nil
-
end
-
-
# Sets the header of the message object.
-
#
-
# Example:
-
#
-
# mail.header = 'To: mikel@test.lindsaar.net\r\nFrom: Bob@bob.com'
-
# mail.header #=> <#Mail::Header
-
2
def header=(value)
-
@header = Mail::Header.new(value, charset)
-
end
-
-
# Returns the header object of the message object. Or, if passed
-
# a parameter sets the value.
-
#
-
# Example:
-
#
-
# mail = Mail::Message.new('To: mikel\r\nFrom: you')
-
# mail.header #=> #<Mail::Header:0x13ce14 @raw_source="To: mikel\r\nFr...
-
#
-
# mail.header #=> nil
-
# mail.header 'To: mikel\r\nFrom: you'
-
# mail.header #=> #<Mail::Header:0x13ce14 @raw_source="To: mikel\r\nFr...
-
2
def header(value = nil)
-
value ? self.header = value : @header
-
end
-
-
# Provides a way to set custom headers, by passing in a hash
-
2
def headers(hash = {})
-
hash.each_pair do |k,v|
-
header[k] = v
-
end
-
end
-
-
# Returns a list of parser errors on the header, each field that had an error
-
# will be reparsed as an unstructured field to preserve the data inside, but
-
# will not be used for further processing.
-
#
-
# It returns a nested array of [field_name, value, original_error_message]
-
# per error found.
-
#
-
# Example:
-
#
-
# message = Mail.new("Content-Transfer-Encoding: weirdo\r\n")
-
# message.errors.size #=> 1
-
# message.errors.first[0] #=> "Content-Transfer-Encoding"
-
# message.errors.first[1] #=> "weirdo"
-
# message.errors.first[3] #=> <The original error message exception>
-
#
-
# This is a good first defence on detecting spam by the way. Some spammers send
-
# invalid emails to try and get email parsers to give up parsing them.
-
2
def errors
-
header.errors
-
end
-
-
# Returns the Bcc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc #=> ['mikel@test.lindsaar.net']
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc << 'ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def bcc( val = nil )
-
default :bcc, val
-
end
-
-
# Sets the Bcc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.bcc #=> ['mikel@test.lindsaar.net']
-
# mail.bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def bcc=( val )
-
header[:bcc] = val
-
end
-
-
# Returns the Cc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc #=> ['mikel@test.lindsaar.net']
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc << 'ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def cc( val = nil )
-
default :cc, val
-
end
-
-
# Sets the Cc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.cc #=> ['mikel@test.lindsaar.net']
-
# mail.cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def cc=( val )
-
header[:cc] = val
-
end
-
-
2
def comments( val = nil )
-
default :comments, val
-
end
-
-
2
def comments=( val )
-
header[:comments] = val
-
end
-
-
2
def content_description( val = nil )
-
default :content_description, val
-
end
-
-
2
def content_description=( val )
-
header[:content_description] = val
-
end
-
-
2
def content_disposition( val = nil )
-
default :content_disposition, val
-
end
-
-
2
def content_disposition=( val )
-
header[:content_disposition] = val
-
end
-
-
2
def content_id( val = nil )
-
default :content_id, val
-
end
-
-
2
def content_id=( val )
-
header[:content_id] = val
-
end
-
-
2
def content_location( val = nil )
-
default :content_location, val
-
end
-
-
2
def content_location=( val )
-
header[:content_location] = val
-
end
-
-
2
def content_transfer_encoding( val = nil )
-
default :content_transfer_encoding, val
-
end
-
-
2
def content_transfer_encoding=( val )
-
header[:content_transfer_encoding] = val
-
end
-
-
2
def content_type( val = nil )
-
default :content_type, val
-
end
-
-
2
def content_type=( val )
-
header[:content_type] = val
-
end
-
-
2
def date( val = nil )
-
default :date, val
-
end
-
-
2
def date=( val )
-
header[:date] = val
-
end
-
-
2
def transport_encoding( val = nil)
-
if val
-
self.transport_encoding = val
-
else
-
@transport_encoding
-
end
-
end
-
-
2
def transport_encoding=( val )
-
@transport_encoding = Mail::Encodings.get_encoding(val)
-
end
-
-
# Returns the From value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from #=> ['mikel@test.lindsaar.net']
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from << 'ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def from( val = nil )
-
default :from, val
-
end
-
-
# Sets the From value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.from #=> ['mikel@test.lindsaar.net']
-
# mail.from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def from=( val )
-
header[:from] = val
-
end
-
-
2
def in_reply_to( val = nil )
-
default :in_reply_to, val
-
end
-
-
2
def in_reply_to=( val )
-
header[:in_reply_to] = val
-
end
-
-
2
def keywords( val = nil )
-
default :keywords, val
-
end
-
-
2
def keywords=( val )
-
header[:keywords] = val
-
end
-
-
# Returns the Message-ID of the mail object. Note, per RFC 2822 the Message ID
-
# consists of what is INSIDE the < > usually seen in the mail header, so this method
-
# will return only what is inside.
-
#
-
# Example:
-
#
-
# mail.message_id = '<1234@message.id>'
-
# mail.message_id #=> '1234@message.id'
-
#
-
# Also allows you to set the Message-ID by passing a string as a parameter
-
#
-
# mail.message_id '<1234@message.id>'
-
# mail.message_id #=> '1234@message.id'
-
2
def message_id( val = nil )
-
default :message_id, val
-
end
-
-
# Sets the Message-ID. Note, per RFC 2822 the Message ID consists of what is INSIDE
-
# the < > usually seen in the mail header, so this method will return only what is inside.
-
#
-
# mail.message_id = '<1234@message.id>'
-
# mail.message_id #=> '1234@message.id'
-
2
def message_id=( val )
-
header[:message_id] = val
-
end
-
-
# Returns the MIME version of the email as a string
-
#
-
# Example:
-
#
-
# mail.mime_version = '1.0'
-
# mail.mime_version #=> '1.0'
-
#
-
# Also allows you to set the MIME version by passing a string as a parameter.
-
#
-
# Example:
-
#
-
# mail.mime_version '1.0'
-
# mail.mime_version #=> '1.0'
-
2
def mime_version( val = nil )
-
default :mime_version, val
-
end
-
-
# Sets the MIME version of the email by accepting a string
-
#
-
# Example:
-
#
-
# mail.mime_version = '1.0'
-
# mail.mime_version #=> '1.0'
-
2
def mime_version=( val )
-
header[:mime_version] = val
-
end
-
-
2
def received( val = nil )
-
if val
-
header[:received] = val
-
else
-
header[:received]
-
end
-
end
-
-
2
def received=( val )
-
header[:received] = val
-
end
-
-
2
def references( val = nil )
-
default :references, val
-
end
-
-
2
def references=( val )
-
header[:references] = val
-
end
-
-
# Returns the Reply-To value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net']
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.reply_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.reply_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to << 'ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def reply_to( val = nil )
-
default :reply_to, val
-
end
-
-
# Sets the Reply-To value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net']
-
# mail.reply_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.reply_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def reply_to=( val )
-
header[:reply_to] = val
-
end
-
-
# Returns the Resent-Bcc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_bcc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc << 'ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_bcc( val = nil )
-
default :resent_bcc, val
-
end
-
-
# Sets the Resent-Bcc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_bcc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_bcc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_bcc=( val )
-
header[:resent_bcc] = val
-
end
-
-
# Returns the Resent-Cc value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_cc 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc << 'ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_cc( val = nil )
-
default :resent_cc, val
-
end
-
-
# Sets the Resent-Cc value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net']
-
# mail.resent_cc = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_cc #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_cc=( val )
-
header[:resent_cc] = val
-
end
-
-
2
def resent_date( val = nil )
-
default :resent_date, val
-
end
-
-
2
def resent_date=( val )
-
header[:resent_date] = val
-
end
-
-
# Returns the Resent-From value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net']
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_from ['Mikel <mikel@test.lindsaar.net>']
-
# mail.resent_from #=> 'mikel@test.lindsaar.net'
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_from << 'ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_from( val = nil )
-
default :resent_from, val
-
end
-
-
# Sets the Resent-From value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net']
-
# mail.resent_from = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_from #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_from=( val )
-
header[:resent_from] = val
-
end
-
-
2
def resent_message_id( val = nil )
-
default :resent_message_id, val
-
end
-
-
2
def resent_message_id=( val )
-
header[:resent_message_id] = val
-
end
-
-
# Returns the Resent-Sender value of the mail object, as a single string of an address
-
# spec. A sender per RFC 2822 must be a single address, so you can not append to
-
# this address.
-
#
-
# Example:
-
#
-
# mail.resent_sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_sender 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
-
2
def resent_sender( val = nil )
-
default :resent_sender, val
-
end
-
-
# Sets the Resent-Sender value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_sender #=> 'mikel@test.lindsaar.net'
-
2
def resent_sender=( val )
-
header[:resent_sender] = val
-
end
-
-
# Returns the Resent-To value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net']
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.resent_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.resent_to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to << 'ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_to( val = nil )
-
default :resent_to, val
-
end
-
-
# Sets the Resent-To value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net']
-
# mail.resent_to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.resent_to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def resent_to=( val )
-
header[:resent_to] = val
-
end
-
-
# Returns the return path of the mail object, or sets it if you pass a string
-
2
def return_path( val = nil )
-
default :return_path, val
-
end
-
-
# Sets the return path of the object
-
2
def return_path=( val )
-
header[:return_path] = val
-
end
-
-
# Returns the Sender value of the mail object, as a single string of an address
-
# spec. A sender per RFC 2822 must be a single address.
-
#
-
# Example:
-
#
-
# mail.sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.sender 'Mikel <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
2
def sender( val = nil )
-
default :sender, val
-
end
-
-
# Sets the Sender value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.sender = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.sender #=> 'mikel@test.lindsaar.net'
-
2
def sender=( val )
-
header[:sender] = val
-
end
-
-
# Returns the SMTP Envelope From value of the mail object, as a single
-
# string of an address spec.
-
#
-
# Defaults to Return-Path, Sender, or the first From address.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_from 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
-
2
def smtp_envelope_from( val = nil )
-
if val
-
self.smtp_envelope_from = val
-
else
-
@smtp_envelope_from || return_path || sender || from_addrs.first
-
end
-
end
-
-
# Sets the From address on the SMTP Envelope.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_from = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_from #=> 'mikel@test.lindsaar.net'
-
2
def smtp_envelope_from=( val )
-
@smtp_envelope_from = val
-
end
-
-
# Returns the SMTP Envelope To value of the mail object.
-
#
-
# Defaults to #destinations: To, Cc, and Bcc addresses.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_to #=> 'mikel@test.lindsaar.net'
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_to ['Mikel <mikel@test.lindsaar.net>', 'Lindsaar <lindsaar@test.lindsaar.net>']
-
# mail.smtp_envelope_to #=> ['mikel@test.lindsaar.net', 'lindsaar@test.lindsaar.net']
-
2
def smtp_envelope_to( val = nil )
-
if val
-
self.smtp_envelope_to = val
-
else
-
@smtp_envelope_to || destinations
-
end
-
end
-
-
# Sets the To addresses on the SMTP Envelope.
-
#
-
# Example:
-
#
-
# mail.smtp_envelope_to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.smtp_envelope_to #=> 'mikel@test.lindsaar.net'
-
#
-
# mail.smtp_envelope_to = ['Mikel <mikel@test.lindsaar.net>', 'Lindsaar <lindsaar@test.lindsaar.net>']
-
# mail.smtp_envelope_to #=> ['mikel@test.lindsaar.net', 'lindsaar@test.lindsaar.net']
-
2
def smtp_envelope_to=( val )
-
@smtp_envelope_to =
-
case val
-
when Array, NilClass
-
val
-
else
-
[val]
-
end
-
end
-
-
# Returns the decoded value of the subject field, as a single string.
-
#
-
# Example:
-
#
-
# mail.subject = "G'Day mate"
-
# mail.subject #=> "G'Day mate"
-
# mail.subject = '=?UTF-8?Q?This_is_=E3=81=82_string?='
-
# mail.subject #=> "This is あ string"
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.subject "G'Day mate"
-
# mail.subject #=> "G'Day mate"
-
2
def subject( val = nil )
-
default :subject, val
-
end
-
-
# Sets the Subject value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.subject = '=?UTF-8?Q?This_is_=E3=81=82_string?='
-
# mail.subject #=> "This is あ string"
-
2
def subject=( val )
-
header[:subject] = val
-
end
-
-
# Returns the To value of the mail object as an array of strings of
-
# address specs.
-
#
-
# Example:
-
#
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to #=> ['mikel@test.lindsaar.net']
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
#
-
# Also allows you to set the value by passing a value as a parameter
-
#
-
# Example:
-
#
-
# mail.to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to #=> ['mikel@test.lindsaar.net']
-
#
-
# Additionally, you can append new addresses to the returned Array like
-
# object.
-
#
-
# Example:
-
#
-
# mail.to 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to << 'ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def to( val = nil )
-
default :to, val
-
end
-
-
# Sets the To value of the mail object, pass in a string of the field
-
#
-
# Example:
-
#
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>'
-
# mail.to #=> ['mikel@test.lindsaar.net']
-
# mail.to = 'Mikel <mikel@test.lindsaar.net>, ada@test.lindsaar.net'
-
# mail.to #=> ['mikel@test.lindsaar.net', 'ada@test.lindsaar.net']
-
2
def to=( val )
-
header[:to] = val
-
end
-
-
# Returns the default value of the field requested as a symbol.
-
#
-
# Each header field has a :default method which returns the most common use case for
-
# that field, for example, the date field types will return a DateTime object when
-
# sent :default, the subject, or unstructured fields will return a decoded string of
-
# their value, the address field types will return a single addr_spec or an array of
-
# addr_specs if there is more than one.
-
2
def default( sym, val = nil )
-
if val
-
header[sym] = val
-
else
-
header[sym].default if header[sym]
-
end
-
end
-
-
# Sets the body object of the message object.
-
#
-
# Example:
-
#
-
# mail.body = 'This is the body'
-
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is the bo...
-
#
-
# You can also reset the body of an Message object by setting body to nil
-
#
-
# Example:
-
#
-
# mail.body = 'this is the body'
-
# mail.body.encoded #=> 'this is the body'
-
# mail.body = nil
-
# mail.body.encoded #=> ''
-
#
-
# If you try and set the body of an email that is a multipart email, then instead
-
# of deleting all the parts of your email, mail will add a text/plain part to
-
# your email:
-
#
-
# mail.add_file 'somefilename.png'
-
# mail.parts.length #=> 1
-
# mail.body = "This is a body"
-
# mail.parts.length #=> 2
-
# mail.parts.last.content_type.content_type #=> 'This is a body'
-
2
def body=(value)
-
body_lazy(value)
-
end
-
-
# Returns the body of the message object. Or, if passed
-
# a parameter sets the value.
-
#
-
# Example:
-
#
-
# mail = Mail::Message.new('To: mikel\r\n\r\nThis is the body')
-
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is the bo...
-
#
-
# mail.body 'This is another body'
-
# mail.body #=> #<Mail::Body:0x13919c @raw_source="This is anothe...
-
2
def body(value = nil)
-
if value
-
self.body = value
-
# add_encoding_to_body
-
else
-
process_body_raw if @body_raw
-
@body
-
end
-
end
-
-
2
def body_encoding(value)
-
if value.nil?
-
body.encoding
-
else
-
body.encoding = value
-
end
-
end
-
-
2
def body_encoding=(value)
-
body.encoding = value
-
end
-
-
# Returns the list of addresses this message should be sent to by
-
# collecting the addresses off the to, cc and bcc fields.
-
#
-
# Example:
-
#
-
# mail.to = 'mikel@test.lindsaar.net'
-
# mail.cc = 'sam@test.lindsaar.net'
-
# mail.bcc = 'bob@test.lindsaar.net'
-
# mail.destinations.length #=> 3
-
# mail.destinations.first #=> 'mikel@test.lindsaar.net'
-
2
def destinations
-
[to_addrs, cc_addrs, bcc_addrs].compact.flatten
-
end
-
-
# Returns an array of addresses (the encoded value) in the From field,
-
# if no From field, returns an empty array
-
2
def from_addrs
-
from ? [from].flatten : []
-
end
-
-
# Returns an array of addresses (the encoded value) in the To field,
-
# if no To field, returns an empty array
-
2
def to_addrs
-
to ? [to].flatten : []
-
end
-
-
# Returns an array of addresses (the encoded value) in the Cc field,
-
# if no Cc field, returns an empty array
-
2
def cc_addrs
-
cc ? [cc].flatten : []
-
end
-
-
# Returns an array of addresses (the encoded value) in the Bcc field,
-
# if no Bcc field, returns an empty array
-
2
def bcc_addrs
-
bcc ? [bcc].flatten : []
-
end
-
-
# Allows you to add an arbitrary header
-
#
-
# Example:
-
#
-
# mail['foo'] = '1234'
-
# mail['foo'].to_s #=> '1234'
-
2
def []=(name, value)
-
if name.to_s == 'body'
-
self.body = value
-
elsif name.to_s =~ /content[-_]type/i
-
header[name] = value
-
elsif name.to_s == 'charset'
-
self.charset = value
-
else
-
header[name] = value
-
end
-
end
-
-
# Allows you to read an arbitrary header
-
#
-
# Example:
-
#
-
# mail['foo'] = '1234'
-
# mail['foo'].to_s #=> '1234'
-
2
def [](name)
-
header[underscoreize(name)]
-
end
-
-
# Method Missing in this implementation allows you to set any of the
-
# standard fields directly as you would the "to", "subject" etc.
-
#
-
# Those fields used most often (to, subject et al) are given their
-
# own method for ease of documentation and also to avoid the hook
-
# call to method missing.
-
#
-
# This will only catch the known fields listed in:
-
#
-
# Mail::Field::KNOWN_FIELDS
-
#
-
# as per RFC 2822, any ruby string or method name could pretty much
-
# be a field name, so we don't want to just catch ANYTHING sent to
-
# a message object and interpret it as a header.
-
#
-
# This method provides all three types of header call to set, read
-
# and explicitly set with the = operator
-
#
-
# Examples:
-
#
-
# mail.comments = 'These are some comments'
-
# mail.comments #=> 'These are some comments'
-
#
-
# mail.comments 'These are other comments'
-
# mail.comments #=> 'These are other comments'
-
#
-
#
-
# mail.date = 'Tue, 1 Jul 2003 10:52:37 +0200'
-
# mail.date.to_s #=> 'Tue, 1 Jul 2003 10:52:37 +0200'
-
#
-
# mail.date 'Tue, 1 Jul 2003 10:52:37 +0200'
-
# mail.date.to_s #=> 'Tue, 1 Jul 2003 10:52:37 +0200'
-
#
-
#
-
# mail.resent_msg_id = '<1234@resent_msg_id.lindsaar.net>'
-
# mail.resent_msg_id #=> '<1234@resent_msg_id.lindsaar.net>'
-
#
-
# mail.resent_msg_id '<4567@resent_msg_id.lindsaar.net>'
-
# mail.resent_msg_id #=> '<4567@resent_msg_id.lindsaar.net>'
-
2
def method_missing(name, *args, &block)
-
#:nodoc:
-
# Only take the structured fields, as we could take _anything_ really
-
# as it could become an optional field... "but therin lies the dark side"
-
field_name = underscoreize(name).chomp("=")
-
if Mail::Field::KNOWN_FIELDS.include?(field_name)
-
if args.empty?
-
header[field_name]
-
else
-
header[field_name] = args.first
-
end
-
else
-
super # otherwise pass it on
-
end
-
#:startdoc:
-
end
-
-
# Returns an FieldList of all the fields in the header in the order that
-
# they appear in the header
-
2
def header_fields
-
header.fields
-
end
-
-
# Returns true if the message has a message ID field, the field may or may
-
# not have a value, but the field exists or not.
-
2
def has_message_id?
-
header.has_message_id?
-
end
-
-
# Returns true if the message has a Date field, the field may or may
-
# not have a value, but the field exists or not.
-
2
def has_date?
-
header.has_date?
-
end
-
-
# Returns true if the message has a Mime-Version field, the field may or may
-
# not have a value, but the field exists or not.
-
2
def has_mime_version?
-
header.has_mime_version?
-
end
-
-
2
def has_content_type?
-
tmp = header[:content_type].main_type rescue nil
-
!!tmp
-
end
-
-
2
def has_charset?
-
tmp = header[:content_type].parameters rescue nil
-
!!(has_content_type? && tmp && tmp['charset'])
-
end
-
-
2
def has_content_transfer_encoding?
-
header[:content_transfer_encoding] && Utilities.blank?(header[:content_transfer_encoding].errors)
-
end
-
-
2
def has_transfer_encoding? # :nodoc:
-
STDERR.puts(":has_transfer_encoding? is deprecated in Mail 1.4.3. Please use has_content_transfer_encoding?\n#{caller}")
-
has_content_transfer_encoding?
-
end
-
-
# Creates a new empty Message-ID field and inserts it in the correct order
-
# into the Header. The MessageIdField object will automatically generate
-
# a unique message ID if you try and encode it or output it to_s without
-
# specifying a message id.
-
#
-
# It will preserve the message ID you specify if you do.
-
2
def add_message_id(msg_id_val = '')
-
header['message-id'] = msg_id_val
-
end
-
-
# Creates a new empty Date field and inserts it in the correct order
-
# into the Header. The DateField object will automatically generate
-
# DateTime.now's date if you try and encode it or output it to_s without
-
# specifying a date yourself.
-
#
-
# It will preserve any date you specify if you do.
-
2
def add_date(date_val = '')
-
header['date'] = date_val
-
end
-
-
# Creates a new empty Mime Version field and inserts it in the correct order
-
# into the Header. The MimeVersion object will automatically generate
-
# set itself to '1.0' if you try and encode it or output it to_s without
-
# specifying a version yourself.
-
#
-
# It will preserve any date you specify if you do.
-
2
def add_mime_version(ver_val = '')
-
header['mime-version'] = ver_val
-
end
-
-
# Adds a content type and charset if the body is US-ASCII
-
#
-
# Otherwise raises a warning
-
2
def add_content_type
-
header[:content_type] = 'text/plain'
-
end
-
-
# Adds a content type and charset if the body is US-ASCII
-
#
-
# Otherwise raises a warning
-
2
def add_charset
-
if !body.empty?
-
# Only give a warning if this isn't an attachment, has non US-ASCII and the user
-
# has not specified an encoding explicitly.
-
if @defaulted_charset && body.raw_source.not_ascii_only? && !self.attachment?
-
warning = "Non US-ASCII detected and no charset defined.\nDefaulting to UTF-8, set your own if this is incorrect.\n"
-
STDERR.puts(warning)
-
end
-
header[:content_type].parameters['charset'] = @charset
-
end
-
end
-
-
# Adds a content transfer encoding
-
#
-
# Otherwise raises a warning
-
2
def add_content_transfer_encoding
-
if body.only_us_ascii?
-
header[:content_transfer_encoding] = '7bit'
-
else
-
warning = "Non US-ASCII detected and no content-transfer-encoding defined.\nDefaulting to 8bit, set your own if this is incorrect.\n"
-
STDERR.puts(warning)
-
header[:content_transfer_encoding] = '8bit'
-
end
-
end
-
-
2
def add_transfer_encoding # :nodoc:
-
STDERR.puts(":add_transfer_encoding is deprecated in Mail 1.4.3. Please use add_content_transfer_encoding\n#{caller}")
-
add_content_transfer_encoding
-
end
-
-
2
def transfer_encoding # :nodoc:
-
STDERR.puts(":transfer_encoding is deprecated in Mail 1.4.3. Please use content_transfer_encoding\n#{caller}")
-
content_transfer_encoding
-
end
-
-
# Returns the MIME media type of part we are on, this is taken from the content-type header
-
2
def mime_type
-
has_content_type? ? header[:content_type].string : nil rescue nil
-
end
-
-
2
def message_content_type
-
STDERR.puts(":message_content_type is deprecated in Mail 1.4.3. Please use mime_type\n#{caller}")
-
mime_type
-
end
-
-
# Returns the character set defined in the content type field
-
2
def charset
-
if @header
-
has_content_type? ? content_type_parameters['charset'] : @charset
-
else
-
@charset
-
end
-
end
-
-
# Sets the charset to the supplied value.
-
2
def charset=(value)
-
@defaulted_charset = false
-
@charset = value
-
@header.charset = value
-
end
-
-
# Returns the main content type
-
2
def main_type
-
has_content_type? ? header[:content_type].main_type : nil rescue nil
-
end
-
-
# Returns the sub content type
-
2
def sub_type
-
has_content_type? ? header[:content_type].sub_type : nil rescue nil
-
end
-
-
# Returns the content type parameters
-
2
def mime_parameters
-
STDERR.puts(':mime_parameters is deprecated in Mail 1.4.3, please use :content_type_parameters instead')
-
content_type_parameters
-
end
-
-
# Returns the content type parameters
-
2
def content_type_parameters
-
has_content_type? ? header[:content_type].parameters : nil rescue nil
-
end
-
-
# Returns true if the message is multipart
-
2
def multipart?
-
has_content_type? ? !!(main_type =~ /^multipart$/i) : false
-
end
-
-
# Returns true if the message is a multipart/report
-
2
def multipart_report?
-
multipart? && sub_type =~ /^report$/i
-
end
-
-
# Returns true if the message is a multipart/report; report-type=delivery-status;
-
2
def delivery_status_report?
-
multipart_report? && content_type_parameters['report-type'] =~ /^delivery-status$/i
-
end
-
-
# returns the part in a multipart/report email that has the content-type delivery-status
-
2
def delivery_status_part
-
@delivery_stats_part ||= parts.select { |p| p.delivery_status_report_part? }.first
-
end
-
-
2
def bounced?
-
delivery_status_part and delivery_status_part.bounced?
-
end
-
-
2
def action
-
delivery_status_part and delivery_status_part.action
-
end
-
-
2
def final_recipient
-
delivery_status_part and delivery_status_part.final_recipient
-
end
-
-
2
def error_status
-
delivery_status_part and delivery_status_part.error_status
-
end
-
-
2
def diagnostic_code
-
delivery_status_part and delivery_status_part.diagnostic_code
-
end
-
-
2
def remote_mta
-
delivery_status_part and delivery_status_part.remote_mta
-
end
-
-
2
def retryable?
-
delivery_status_part and delivery_status_part.retryable?
-
end
-
-
# Returns the current boundary for this message part
-
2
def boundary
-
content_type_parameters ? content_type_parameters['boundary'] : nil
-
end
-
-
# Returns a parts list object of all the parts in the message
-
2
def parts
-
body.parts
-
end
-
-
# Returns an AttachmentsList object, which holds all of the attachments in
-
# the receiver object (either the entire email or a part within) and all
-
# of its descendants.
-
#
-
# It also allows you to add attachments to the mail object directly, like so:
-
#
-
# mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
-
#
-
# If you do this, then Mail will take the file name and work out the MIME media type
-
# set the Content-Type, Content-Disposition, Content-Transfer-Encoding and
-
# base64 encode the contents of the attachment all for you.
-
#
-
# You can also specify overrides if you want by passing a hash instead of a string:
-
#
-
# mail.attachments['filename.jpg'] = {:mime_type => 'application/x-gzip',
-
# :content => File.read('/path/to/filename.jpg')}
-
#
-
# If you want to use a different encoding than Base64, you can pass an encoding in,
-
# but then it is up to you to pass in the content pre-encoded, and don't expect
-
# Mail to know how to decode this data:
-
#
-
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
-
# mail.attachments['filename.jpg'] = {:mime_type => 'application/x-gzip',
-
# :encoding => 'SpecialEncoding',
-
# :content => file_content }
-
#
-
# You can also search for specific attachments:
-
#
-
# # By Filename
-
# mail.attachments['filename.jpg'] #=> Mail::Part object or nil
-
#
-
# # or by index
-
# mail.attachments[0] #=> Mail::Part (first attachment)
-
#
-
2
def attachments
-
parts.attachments
-
end
-
-
2
def has_attachments?
-
!attachments.empty?
-
end
-
-
# Accessor for html_part
-
2
def html_part(&block)
-
if block_given?
-
self.html_part = Mail::Part.new(:content_type => 'text/html', &block)
-
else
-
@html_part || find_first_mime_type('text/html')
-
end
-
end
-
-
# Accessor for text_part
-
2
def text_part(&block)
-
if block_given?
-
self.text_part = Mail::Part.new(:content_type => 'text/plain', &block)
-
else
-
@text_part || find_first_mime_type('text/plain')
-
end
-
end
-
-
# Helper to add a html part to a multipart/alternative email. If this and
-
# text_part are both defined in a message, then it will be a multipart/alternative
-
# message and set itself that way.
-
2
def html_part=(msg)
-
# Assign the html part and set multipart/alternative if there's a text part.
-
if msg
-
msg = Mail::Part.new(:body => msg) unless msg.kind_of?(Mail::Message)
-
-
@html_part = msg
-
@html_part.content_type = 'text/html' unless @html_part.has_content_type?
-
add_multipart_alternate_header if text_part
-
add_part @html_part
-
-
# If nil, delete the html part and back out of multipart/alternative.
-
elsif @html_part
-
parts.delete_if { |p| p.object_id == @html_part.object_id }
-
@html_part = nil
-
if text_part
-
self.content_type = nil
-
body.boundary = nil
-
end
-
end
-
end
-
-
# Helper to add a text part to a multipart/alternative email. If this and
-
# html_part are both defined in a message, then it will be a multipart/alternative
-
# message and set itself that way.
-
2
def text_part=(msg)
-
# Assign the text part and set multipart/alternative if there's an html part.
-
if msg
-
msg = Mail::Part.new(:body => msg) unless msg.kind_of?(Mail::Message)
-
-
@text_part = msg
-
@text_part.content_type = 'text/plain' unless @text_part.has_content_type?
-
add_multipart_alternate_header if html_part
-
add_part @text_part
-
-
# If nil, delete the text part and back out of multipart/alternative.
-
elsif @text_part
-
parts.delete_if { |p| p.object_id == @text_part.object_id }
-
@text_part = nil
-
if html_part
-
self.content_type = nil
-
body.boundary = nil
-
end
-
end
-
end
-
-
# Adds a part to the parts list or creates the part list
-
2
def add_part(part)
-
if !body.multipart? && !Utilities.blank?(self.body.decoded)
-
@text_part = Mail::Part.new('Content-Type: text/plain;')
-
@text_part.body = body.decoded
-
self.body << @text_part
-
add_multipart_alternate_header
-
end
-
add_boundary
-
self.body << part
-
end
-
-
# Allows you to add a part in block form to an existing mail message object
-
#
-
# Example:
-
#
-
# mail = Mail.new do
-
# part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
-
# p.part :content_type => "text/plain", :body => "test text\nline #2"
-
# p.part :content_type => "text/html", :body => "<b>test</b> HTML<br/>\nline #2"
-
# end
-
# end
-
2
def part(params = {})
-
new_part = Part.new(params)
-
yield new_part if block_given?
-
add_part(new_part)
-
end
-
-
# Adds a file to the message. You have two options with this method, you can
-
# just pass in the absolute path to the file you want and Mail will read the file,
-
# get the filename from the path you pass in and guess the MIME media type, or you
-
# can pass in the filename as a string, and pass in the file content as a blob.
-
#
-
# Example:
-
#
-
# m = Mail.new
-
# m.add_file('/path/to/filename.png')
-
#
-
# m = Mail.new
-
# m.add_file(:filename => 'filename.png', :content => File.read('/path/to/file.jpg'))
-
#
-
# Note also that if you add a file to an existing message, Mail will convert that message
-
# to a MIME multipart email, moving whatever plain text body you had into its own text
-
# plain part.
-
#
-
# Example:
-
#
-
# m = Mail.new do
-
# body 'this is some text'
-
# end
-
# m.multipart? #=> false
-
# m.add_file('/path/to/filename.png')
-
# m.multipart? #=> true
-
# m.parts.first.content_type.content_type #=> 'text/plain'
-
# m.parts.last.content_type.content_type #=> 'image/png'
-
#
-
# See also #attachments
-
2
def add_file(values)
-
convert_to_multipart unless self.multipart? || Utilities.blank?(self.body.decoded)
-
add_multipart_mixed_header
-
if values.is_a?(String)
-
basename = File.basename(values)
-
filedata = File.open(values, 'rb') { |f| f.read }
-
else
-
basename = values[:filename]
-
filedata = values
-
unless filedata[:content]
-
filedata = values.merge(:content=>File.open(values[:filename], 'rb') { |f| f.read })
-
end
-
end
-
self.attachments[basename] = filedata
-
end
-
-
2
def convert_to_multipart
-
text = body.decoded
-
self.body = ''
-
text_part = Mail::Part.new({:content_type => 'text/plain;',
-
:body => text})
-
text_part.charset = charset unless @defaulted_charset
-
self.body << text_part
-
end
-
-
# Encodes the message, calls encode on all its parts, gets an email message
-
# ready to send
-
2
def ready_to_send!
-
identify_and_set_transfer_encoding
-
parts.sort!([ "text/plain", "text/enriched", "text/html", "multipart/alternative" ])
-
parts.each do |part|
-
part.transport_encoding = transport_encoding
-
part.ready_to_send!
-
end
-
add_required_fields
-
end
-
-
2
def encode!
-
STDERR.puts("Deprecated in 1.1.0 in favour of :ready_to_send! as it is less confusing with encoding and decoding.")
-
ready_to_send!
-
end
-
-
# Outputs an encoded string representation of the mail message including
-
# all headers, attachments, etc. This is an encoded email in US-ASCII,
-
# so it is able to be directly sent to an email server.
-
2
def encoded
-
ready_to_send!
-
buffer = header.encoded
-
buffer << "\r\n"
-
buffer << body.encoded(content_transfer_encoding)
-
buffer
-
end
-
-
2
def without_attachments!
-
return self unless has_attachments?
-
-
parts.delete_if { |p| p.attachment? }
-
body_raw = if parts.empty?
-
''
-
else
-
body.encoded
-
end
-
-
@body = Mail::Body.new(body_raw)
-
-
self
-
end
-
-
2
def to_yaml(opts = {})
-
hash = {}
-
hash['headers'] = {}
-
header.fields.each do |field|
-
hash['headers'][field.name] = field.value
-
end
-
hash['delivery_handler'] = delivery_handler.to_s if delivery_handler
-
hash['transport_encoding'] = transport_encoding.to_s
-
special_variables = [:@header, :@delivery_handler, :@transport_encoding]
-
if multipart?
-
hash['multipart_body'] = []
-
body.parts.map { |part| hash['multipart_body'] << part.to_yaml }
-
special_variables.push(:@body, :@text_part, :@html_part)
-
end
-
(instance_variables.map(&:to_sym) - special_variables).each do |var|
-
hash[var.to_s] = instance_variable_get(var)
-
end
-
hash.to_yaml(opts)
-
end
-
-
2
def self.from_yaml(str)
-
hash = YAML.load(str)
-
m = self.new(:headers => hash['headers'])
-
hash.delete('headers')
-
hash.each do |k,v|
-
case
-
when k == 'delivery_handler'
-
begin
-
m.delivery_handler = Object.const_get(v) unless Utilities.blank?(v)
-
rescue NameError
-
end
-
when k == 'transport_encoding'
-
m.transport_encoding(v)
-
when k == 'multipart_body'
-
v.map {|part| m.add_part Mail::Part.from_yaml(part) }
-
when k =~ /^@/
-
m.instance_variable_set(k.to_sym, v)
-
end
-
end
-
m
-
end
-
-
2
def self.from_hash(hash)
-
Mail::Message.new(hash)
-
end
-
-
2
def to_s
-
encoded
-
end
-
-
2
def inspect
-
"#<#{self.class}:#{self.object_id}, Multipart: #{multipart?}, Headers: #{header.field_summary}>"
-
end
-
-
2
def decoded
-
case
-
when self.text?
-
decode_body_as_text
-
when self.attachment?
-
decode_body
-
when !self.multipart?
-
body.decoded
-
else
-
raise NoMethodError, 'Can not decode an entire message, try calling #decoded on the various fields and body or parts if it is a multipart message.'
-
end
-
end
-
-
2
def read
-
if self.attachment?
-
decode_body
-
else
-
raise NoMethodError, 'Can not call read on a part unless it is an attachment.'
-
end
-
end
-
-
2
def decode_body
-
body.decoded
-
end
-
-
# Returns true if this part is an attachment,
-
# false otherwise.
-
2
def attachment?
-
!!find_attachment
-
end
-
-
# Returns the attachment data if there is any
-
2
def attachment
-
@attachment
-
end
-
-
# Returns the filename of the attachment
-
2
def filename
-
find_attachment
-
end
-
-
2
def all_parts
-
parts.map { |p| [p, p.all_parts] }.flatten
-
end
-
-
2
def find_first_mime_type(mt)
-
all_parts.detect { |p| p.mime_type == mt && !p.attachment? }
-
end
-
-
# Skips the deletion of this message. All other messages
-
# flagged for delete still will be deleted at session close (i.e. when
-
# #find exits). Only has an effect if you're using #find_and_delete
-
# or #find with :delete_after_find set to true.
-
2
def skip_deletion
-
@mark_for_delete = false
-
end
-
-
# Sets whether this message should be deleted at session close (i.e.
-
# after #find). Message will only be deleted if messages are retrieved
-
# using the #find_and_delete method, or by calling #find with
-
# :delete_after_find set to true.
-
2
def mark_for_delete=(value = true)
-
@mark_for_delete = value
-
end
-
-
# Returns whether message will be marked for deletion.
-
# If so, the message will be deleted at session close (i.e. after #find
-
# exits), but only if also using the #find_and_delete method, or by
-
# calling #find with :delete_after_find set to true.
-
#
-
# Side-note: Just to be clear, this method will return true even if
-
# the message hasn't yet been marked for delete on the mail server.
-
# However, if this method returns true, it *will be* marked on the
-
# server after each block yields back to #find or #find_and_delete.
-
2
def is_marked_for_delete?
-
return @mark_for_delete
-
end
-
-
2
def text?
-
has_content_type? ? !!(main_type =~ /^text$/i) : false
-
end
-
-
2
private
-
-
2
HEADER_SEPARATOR = /#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m
-
-
# 2.1. General Description
-
# A message consists of header fields (collectively called "the header
-
# of the message") followed, optionally, by a body. The header is a
-
# sequence of lines of characters with special syntax as defined in
-
# this standard. The body is simply a sequence of characters that
-
# follows the header and is separated from the header by an empty line
-
# (i.e., a line with nothing preceding the CRLF).
-
#
-
# Additionally, I allow for the case where someone might have put whitespace
-
# on the "gap line"
-
2
def parse_message
-
header_part, body_part = raw_source.lstrip.split(HEADER_SEPARATOR, 2)
-
self.header = header_part
-
self.body = body_part
-
end
-
-
2
def raw_source=(value)
-
value = value.dup.force_encoding(Encoding::BINARY) if RUBY_VERSION >= "1.9.1"
-
@raw_source = ::Mail::Utilities.to_crlf(value)
-
end
-
-
# see comments to body=. We take data and process it lazily
-
2
def body_lazy(value)
-
process_body_raw if @body_raw && value
-
case
-
when value == nil || value.length<=0
-
@body = Mail::Body.new('')
-
@body_raw = nil
-
add_encoding_to_body
-
when @body && @body.multipart?
-
@body << Mail::Part.new(value)
-
add_encoding_to_body
-
else
-
@body_raw = value
-
# process_body_raw
-
end
-
end
-
-
-
2
def process_body_raw
-
@body = Mail::Body.new(@body_raw)
-
@body_raw = nil
-
separate_parts if @separate_parts
-
-
add_encoding_to_body
-
end
-
-
2
def set_envelope_header
-
raw_string = raw_source.to_s
-
if match_data = raw_source.to_s.match(/\AFrom\s(#{TEXT}+)#{CRLF}/m)
-
set_envelope(match_data[1])
-
self.raw_source = raw_string.sub(match_data[0], "")
-
end
-
end
-
-
2
def separate_parts
-
body.split!(boundary)
-
end
-
-
2
def add_encoding_to_body
-
if has_content_transfer_encoding?
-
@body.encoding = content_transfer_encoding
-
end
-
end
-
-
2
def identify_and_set_transfer_encoding
-
if body && body.multipart?
-
self.content_transfer_encoding = @transport_encoding
-
else
-
self.content_transfer_encoding = body.get_best_encoding(@transport_encoding)
-
end
-
end
-
-
2
def add_required_fields
-
add_required_message_fields
-
add_multipart_mixed_header if body.multipart?
-
add_content_type unless has_content_type?
-
add_charset if text? && !has_charset?
-
add_content_transfer_encoding unless has_content_transfer_encoding?
-
end
-
-
2
def add_required_message_fields
-
add_date unless has_date?
-
add_mime_version unless has_mime_version?
-
add_message_id unless has_message_id?
-
end
-
-
2
def add_multipart_alternate_header
-
header['content-type'] = ContentTypeField.with_boundary('multipart/alternative').value
-
header['content_type'].parameters[:charset] = @charset
-
body.boundary = boundary
-
end
-
-
2
def add_boundary
-
unless body.boundary && boundary
-
header['content-type'] = 'multipart/mixed' unless header['content-type']
-
header['content-type'].parameters[:boundary] = ContentTypeField.generate_boundary
-
header['content_type'].parameters[:charset] = @charset
-
body.boundary = boundary
-
end
-
end
-
-
2
def add_multipart_mixed_header
-
unless header['content-type']
-
header['content-type'] = ContentTypeField.with_boundary('multipart/mixed').value
-
header['content_type'].parameters[:charset] = @charset
-
body.boundary = boundary
-
end
-
end
-
-
2
def init_with_hash(hash)
-
passed_in_options = IndifferentHash.new(hash)
-
self.raw_source = ''
-
-
@header = Mail::Header.new
-
@body = Mail::Body.new
-
@body_raw = nil
-
-
# We need to store the body until last, as we need all headers added first
-
body_content = nil
-
-
passed_in_options.each_pair do |k,v|
-
k = underscoreize(k).to_sym if k.class == String
-
if k == :headers
-
self.headers(v)
-
elsif k == :body
-
body_content = v
-
else
-
self[k] = v
-
end
-
end
-
-
if body_content
-
self.body = body_content
-
if has_content_transfer_encoding?
-
body.encoding = content_transfer_encoding
-
end
-
end
-
end
-
-
2
def init_with_string(string)
-
self.raw_source = string
-
set_envelope_header
-
parse_message
-
@separate_parts = multipart?
-
end
-
-
# Returns the filename of the attachment (if it exists) or returns nil
-
2
def find_attachment
-
content_type_name = header[:content_type].filename rescue nil
-
content_disp_name = header[:content_disposition].filename rescue nil
-
content_loc_name = header[:content_location].location rescue nil
-
case
-
when content_type && content_type_name
-
filename = content_type_name
-
when content_disposition && content_disp_name
-
filename = content_disp_name
-
when content_location && content_loc_name
-
filename = content_loc_name
-
else
-
filename = nil
-
end
-
filename = Mail::Encodings.decode_encode(filename, :decode) if filename rescue filename
-
filename
-
end
-
-
2
def do_delivery
-
begin
-
if perform_deliveries
-
delivery_method.deliver!(self)
-
end
-
rescue => e # Net::SMTP errors or sendmail pipe errors
-
raise e if raise_delivery_errors
-
end
-
end
-
-
2
def decode_body_as_text
-
Encodings.transcode_charset decode_body, charset, 'UTF-8'
-
end
-
end
-
end
-
# frozen_string_literal: true
-
2
require 'mail/network/retriever_methods/base'
-
-
2
module Mail
-
2
register_autoload :SMTP, 'mail/network/delivery_methods/smtp'
-
2
register_autoload :FileDelivery, 'mail/network/delivery_methods/file_delivery'
-
2
register_autoload :Sendmail, 'mail/network/delivery_methods/sendmail'
-
2
register_autoload :Exim, 'mail/network/delivery_methods/exim'
-
2
register_autoload :SMTPConnection, 'mail/network/delivery_methods/smtp_connection'
-
2
register_autoload :TestMailer, 'mail/network/delivery_methods/test_mailer'
-
-
2
register_autoload :POP3, 'mail/network/retriever_methods/pop3'
-
2
register_autoload :IMAP, 'mail/network/retriever_methods/imap'
-
2
register_autoload :TestRetriever, 'mail/network/retriever_methods/test_retriever'
-
end
-
# frozen_string_literal: true
-
2
require 'mail/check_delivery_params'
-
-
2
module Mail
-
-
# FileDelivery class delivers emails into multiple files based on the destination
-
# address. Each file is appended to if it already exists.
-
#
-
# So if you have an email going to fred@test, bob@test, joe@anothertest, and you
-
# set your location path to /path/to/mails then FileDelivery will create the directory
-
# if it does not exist, and put one copy of the email in three files, called
-
# by their message id
-
#
-
# Make sure the path you specify with :location is writable by the Ruby process
-
# running Mail.
-
2
class FileDelivery
-
2
include Mail::CheckDeliveryParams
-
-
2
if RUBY_VERSION >= '1.9.1'
-
2
require 'fileutils'
-
else
-
require 'ftools'
-
end
-
-
2
def initialize(values)
-
self.settings = { :location => './mails' }.merge!(values)
-
end
-
-
2
attr_accessor :settings
-
-
2
def deliver!(mail)
-
check_delivery_params(mail)
-
-
if ::File.respond_to?(:makedirs)
-
::File.makedirs settings[:location]
-
else
-
::FileUtils.mkdir_p settings[:location]
-
end
-
-
mail.destinations.uniq.each do |to|
-
::File.open(::File.join(settings[:location], File.basename(to.to_s)), 'a') { |f| "#{f.write(mail.encoded)}\r\n\r\n" }
-
end
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
2
require 'mail/check_delivery_params'
-
-
2
module Mail
-
# A delivery method implementation which sends via sendmail.
-
#
-
# To use this, first find out where the sendmail binary is on your computer,
-
# if you are on a mac or unix box, it is usually in /usr/sbin/sendmail, this will
-
# be your sendmail location.
-
#
-
# Mail.defaults do
-
# delivery_method :sendmail
-
# end
-
#
-
# Or if your sendmail binary is not at '/usr/sbin/sendmail'
-
#
-
# Mail.defaults do
-
# delivery_method :sendmail, :location => '/absolute/path/to/your/sendmail'
-
# end
-
#
-
# Then just deliver the email as normal:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# Or by calling deliver on a Mail message
-
#
-
# mail = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# mail.deliver!
-
2
class Sendmail
-
2
include Mail::CheckDeliveryParams
-
-
2
def initialize(values)
-
self.settings = { :location => '/usr/sbin/sendmail',
-
:arguments => '-i' }.merge(values)
-
end
-
-
2
attr_accessor :settings
-
-
2
def deliver!(mail)
-
smtp_from, smtp_to, message = check_delivery_params(mail)
-
-
from = "-f #{self.class.shellquote(smtp_from)}"
-
to = smtp_to.map { |_to| self.class.shellquote(_to) }.join(' ')
-
-
arguments = "#{settings[:arguments]} #{from} --"
-
self.class.call(settings[:location], arguments, to, message)
-
end
-
-
2
def self.call(path, arguments, destinations, encoded_message)
-
popen "#{path} #{arguments} #{destinations}" do |io|
-
io.puts ::Mail::Utilities.to_lf(encoded_message)
-
io.flush
-
end
-
end
-
-
2
if RUBY_VERSION < '1.9.0'
-
def self.popen(command, &block)
-
IO.popen "#{command} 2>&1", 'w+', &block
-
end
-
else
-
2
def self.popen(command, &block)
-
IO.popen command, 'w+', :err => :out, &block
-
end
-
end
-
-
# The following is an adaptation of ruby 1.9.2's shellwords.rb file,
-
# it is modified to include '+' in the allowed list to allow for
-
# sendmail to accept email addresses as the sender with a + in them.
-
2
def self.shellquote(address)
-
# Process as a single byte sequence because not all shell
-
# implementations are multibyte aware.
-
#
-
# A LF cannot be escaped with a backslash because a backslash + LF
-
# combo is regarded as line continuation and simply ignored. Strip it.
-
escaped = address.gsub(/([^A-Za-z0-9_\s\+\-.,:\/@])/n, "\\\\\\1").gsub("\n", '')
-
%("#{escaped}")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
2
require 'mail/check_delivery_params'
-
-
2
module Mail
-
# == Sending Email with SMTP
-
#
-
# Mail allows you to send emails using SMTP. This is done by wrapping Net::SMTP in
-
# an easy to use manner.
-
#
-
# === Sending via SMTP server on Localhost
-
#
-
# Sending locally (to a postfix or sendmail server running on localhost) requires
-
# no special setup. Just to Mail.deliver &block or message.deliver! and it will
-
# be sent in this method.
-
#
-
# === Sending via MobileMe
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "smtp.me.com",
-
# :port => 587,
-
# :domain => 'your.host.name',
-
# :user_name => '<username>',
-
# :password => '<password>',
-
# :authentication => 'plain',
-
# :enable_starttls_auto => true }
-
# end
-
#
-
# === Sending via GMail
-
#
-
# Mail.defaults do
-
# delivery_method :smtp, { :address => "smtp.gmail.com",
-
# :port => 587,
-
# :domain => 'your.host.name',
-
# :user_name => '<username>',
-
# :password => '<password>',
-
# :authentication => 'plain',
-
# :enable_starttls_auto => true }
-
# end
-
#
-
# === Certificate verification
-
#
-
# When using TLS, some mail servers provide certificates that are self-signed
-
# or whose names do not exactly match the hostname given in the address.
-
# OpenSSL will reject these by default. The best remedy is to use the correct
-
# hostname or update the certificate authorities trusted by your ruby. If
-
# that isn't possible, you can control this behavior with
-
# an :openssl_verify_mode setting. Its value may be either an OpenSSL
-
# verify mode constant (OpenSSL::SSL::VERIFY_NONE), or a string containing
-
# the name of an OpenSSL verify mode (none, peer, client_once,
-
# fail_if_no_peer_cert).
-
#
-
# === Others
-
#
-
# Feel free to send me other examples that were tricky
-
#
-
# === Delivering the email
-
#
-
# Once you have the settings right, sending the email is done by:
-
#
-
# Mail.deliver do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# Or by calling deliver on a Mail message
-
#
-
# mail = Mail.new do
-
# to 'mikel@test.lindsaar.net'
-
# from 'ada@test.lindsaar.net'
-
# subject 'testing sendmail'
-
# body 'testing sendmail'
-
# end
-
#
-
# mail.deliver!
-
2
class SMTP
-
2
include Mail::CheckDeliveryParams
-
-
2
def initialize(values)
-
self.settings = { :address => "localhost",
-
:port => 25,
-
:domain => 'localhost.localdomain',
-
:user_name => nil,
-
:password => nil,
-
:authentication => nil,
-
:enable_starttls_auto => true,
-
:openssl_verify_mode => nil,
-
:ssl => nil,
-
:tls => nil
-
}.merge!(values)
-
end
-
-
2
attr_accessor :settings
-
-
# Send the message via SMTP.
-
# The from and to attributes are optional. If not set, they are retrieve from the Message.
-
2
def deliver!(mail)
-
smtp_from, smtp_to, message = check_delivery_params(mail)
-
-
smtp = Net::SMTP.new(settings[:address], settings[:port])
-
if settings[:tls] || settings[:ssl]
-
if smtp.respond_to?(:enable_tls)
-
smtp.enable_tls(ssl_context)
-
end
-
elsif settings[:enable_starttls_auto]
-
if smtp.respond_to?(:enable_starttls_auto)
-
smtp.enable_starttls_auto(ssl_context)
-
end
-
end
-
-
response = nil
-
smtp.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication]) do |smtp_obj|
-
response = smtp_obj.sendmail(message, smtp_from, smtp_to)
-
end
-
-
if settings[:return_response]
-
response
-
else
-
self
-
end
-
end
-
-
-
2
private
-
-
# Allow SSL context to be configured via settings, for Ruby >= 1.9
-
# Just returns openssl verify mode for Ruby 1.8.x
-
2
def ssl_context
-
openssl_verify_mode = settings[:openssl_verify_mode]
-
-
if openssl_verify_mode.kind_of?(String)
-
openssl_verify_mode = "OpenSSL::SSL::VERIFY_#{openssl_verify_mode.upcase}".constantize
-
end
-
-
context = Net::SMTP.default_ssl_context
-
context.verify_mode = openssl_verify_mode
-
context.ca_path = settings[:ca_path] if settings[:ca_path]
-
context.ca_file = settings[:ca_file] if settings[:ca_file]
-
context
-
end
-
end
-
end
-
# frozen_string_literal: true
-
2
require 'mail/check_delivery_params'
-
-
2
module Mail
-
# The TestMailer is a bare bones mailer that does nothing. It is useful
-
# when you are testing.
-
#
-
# It also provides a template of the minimum methods you require to implement
-
# if you want to make a custom mailer for Mail
-
2
class TestMailer
-
2
include Mail::CheckDeliveryParams
-
-
# Provides a store of all the emails sent with the TestMailer so you can check them.
-
2
def TestMailer.deliveries
-
@@deliveries ||= []
-
end
-
-
# Allows you to over write the default deliveries store from an array to some
-
# other object. If you just want to clear the store,
-
# call TestMailer.deliveries.clear.
-
#
-
# If you place another object here, please make sure it responds to:
-
#
-
# * << (message)
-
# * clear
-
# * length
-
# * size
-
# * and other common Array methods
-
2
def TestMailer.deliveries=(val)
-
@@deliveries = val
-
end
-
-
2
def initialize(values)
-
@settings = values.dup
-
end
-
-
2
attr_accessor :settings
-
-
2
def deliver!(mail)
-
check_delivery_params(mail)
-
Mail::TestMailer.deliveries << mail
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
2
module Mail
-
-
2
class Retriever
-
-
# Get the oldest received email(s)
-
#
-
# Possible options:
-
# count: number of emails to retrieve. The default value is 1.
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
#
-
2
def first(options = {}, &block)
-
options ||= {}
-
options[:what] = :first
-
options[:count] ||= 1
-
find(options, &block)
-
end
-
-
# Get the most recent received email(s)
-
#
-
# Possible options:
-
# count: number of emails to retrieve. The default value is 1.
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
#
-
2
def last(options = {}, &block)
-
options ||= {}
-
options[:what] = :last
-
options[:count] ||= 1
-
find(options, &block)
-
end
-
-
# Get all emails.
-
#
-
# Possible options:
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
#
-
2
def all(options = {}, &block)
-
options ||= {}
-
options[:count] = :all
-
find(options, &block)
-
end
-
-
# Find emails in the mailbox, and then deletes them. Without any options, the
-
# five last received emails are returned.
-
#
-
# Possible options:
-
# what: last or first emails. The default is :first.
-
# order: order of emails returned. Possible values are :asc or :desc. Default value is :asc.
-
# count: number of emails to retrieve. The default value is 10. A value of 1 returns an
-
# instance of Message, not an array of Message instances.
-
# delete_after_find: flag for whether to delete each retreived email after find. Default
-
# is true. Call #find if you would like this to default to false.
-
#
-
2
def find_and_delete(options = {}, &block)
-
options ||= {}
-
options[:delete_after_find] ||= true
-
find(options, &block)
-
end
-
-
end
-
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
2
class Part < Message
-
-
# Creates a new empty Content-ID field and inserts it in the correct order
-
# into the Header. The ContentIdField object will automatically generate
-
# a unique content ID if you try and encode it or output it to_s without
-
# specifying a content id.
-
#
-
# It will preserve the content ID you specify if you do.
-
2
def add_content_id(content_id_val = '')
-
header['content-id'] = content_id_val
-
end
-
-
# Returns true if the part has a content ID field, the field may or may
-
# not have a value, but the field exists or not.
-
2
def has_content_id?
-
header.has_content_id?
-
end
-
-
2
def inline_content_id
-
# TODO: Deprecated in 2.2.2 - Remove in 2.3
-
STDERR.puts("Part#inline_content_id is deprecated, please call Part#cid instead")
-
cid
-
end
-
-
2
def cid
-
add_content_id unless has_content_id?
-
uri_escape(unbracket(content_id))
-
end
-
-
2
def url
-
"cid:#{cid}"
-
end
-
-
2
def inline?
-
header[:content_disposition].disposition_type == 'inline' if header[:content_disposition].respond_to?(:disposition_type)
-
end
-
-
2
def add_required_fields
-
super
-
add_content_id if !has_content_id? && inline?
-
end
-
-
2
def add_required_message_fields
-
# Override so we don't add Date, MIME-Version, or Message-ID.
-
end
-
-
2
def delivery_status_report_part?
-
(main_type =~ /message/i && sub_type =~ /delivery-status/i) && body =~ /Status:/
-
end
-
-
2
def delivery_status_data
-
delivery_status_report_part? ? parse_delivery_status_report : {}
-
end
-
-
2
def bounced?
-
if action.is_a?(Array)
-
!!(action.first =~ /failed/i)
-
else
-
!!(action =~ /failed/i)
-
end
-
end
-
-
-
# Either returns the action if the message has just a single report, or an
-
# array of all the actions, one for each report
-
2
def action
-
get_return_values('action')
-
end
-
-
2
def final_recipient
-
get_return_values('final-recipient')
-
end
-
-
2
def error_status
-
get_return_values('status')
-
end
-
-
2
def diagnostic_code
-
get_return_values('diagnostic-code')
-
end
-
-
2
def remote_mta
-
get_return_values('remote-mta')
-
end
-
-
2
def retryable?
-
!(error_status =~ /^5/)
-
end
-
-
2
private
-
-
2
def get_return_values(key)
-
if delivery_status_data[key].is_a?(Array)
-
delivery_status_data[key].map { |a| a.value }
-
elsif !delivery_status_data[key].nil?
-
delivery_status_data[key].value
-
else
-
nil
-
end
-
end
-
-
# A part may not have a header.... so, just init a body if no header
-
2
def parse_message
-
header_part, body_part = raw_source.split(/#{CRLF}#{WSP}*#{CRLF}/m, 2)
-
if header_part =~ HEADER_LINE
-
self.header = header_part
-
self.body = body_part
-
else
-
self.header = "Content-Type: text/plain\r\n"
-
self.body = raw_source
-
end
-
end
-
-
2
def parse_delivery_status_report
-
@delivery_status_data ||= Header.new(body.to_s.gsub("\r\n\r\n", "\r\n"))
-
end
-
-
end
-
-
end
-
# frozen_string_literal: true
-
2
require 'delegate'
-
-
2
module Mail
-
2
class PartsList < DelegateClass(Array)
-
2
attr_reader :parts
-
-
2
def initialize(*args)
-
@parts = Array.new(*args)
-
super @parts
-
end
-
-
# The #encode_with and #to_yaml methods are just implemented
-
# for the sake of backward compatibility ; the delegator does
-
# not correctly delegate these calls to the delegated object
-
2
def encode_with(coder) # :nodoc:
-
coder.represent_object(nil, @parts)
-
end
-
-
2
def to_yaml(options = {}) # :nodoc:
-
@parts.to_yaml(options)
-
end
-
-
2
def attachments
-
Mail::AttachmentsList.new(@parts)
-
end
-
-
2
def collect
-
if block_given?
-
ary = PartsList.new
-
each { |o| ary << yield(o) }
-
ary
-
else
-
to_a
-
end
-
end
-
2
alias_method :map, :collect
-
-
2
def map!
-
raise NoMethodError, "#map! is not defined, please call #collect and create a new PartsList"
-
end
-
-
2
def collect!
-
raise NoMethodError, "#collect! is not defined, please call #collect and create a new PartsList"
-
end
-
-
2
def sort
-
self.class.new(@parts.sort)
-
end
-
-
2
def sort!(order)
-
# stable sort should be used to maintain the relative order as the parts are added
-
i = 0;
-
sorted = @parts.sort_by do |a|
-
# OK, 10000 is arbitrary... if anyone actually wants to explicitly sort 10000 parts of a
-
# single email message... please show me a use case and I'll put more work into this method,
-
# in the meantime, it works :)
-
[get_order_value(a, order), i += 1]
-
end
-
@parts.clear
-
sorted.each { |p| @parts << p }
-
end
-
-
2
private
-
-
2
def get_order_value(part, order)
-
if part.respond_to?(:content_type) && !part[:content_type].nil?
-
order.index(part[:content_type].string.downcase) || 10000
-
else
-
10000
-
end
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
2
module Mail
-
2
module Utilities
-
-
2
LF = "\n"
-
2
CRLF = "\r\n"
-
-
2
include Constants
-
-
# Returns true if the string supplied is free from characters not allowed as an ATOM
-
2
def atom_safe?( str )
-
not ATOM_UNSAFE === str
-
end
-
-
# If the string supplied has ATOM unsafe characters in it, will return the string quoted
-
# in double quotes, otherwise returns the string unmodified
-
2
def quote_atom( str )
-
atom_safe?( str ) ? str : dquote(str)
-
end
-
-
# If the string supplied has PHRASE unsafe characters in it, will return the string quoted
-
# in double quotes, otherwise returns the string unmodified
-
2
def quote_phrase( str )
-
if RUBY_VERSION >= '1.9'
-
original_encoding = str.encoding
-
ascii_str = str.dup.force_encoding('ASCII-8BIT')
-
if (PHRASE_UNSAFE === ascii_str)
-
dquote(ascii_str).force_encoding(original_encoding)
-
else
-
str
-
end
-
else
-
(PHRASE_UNSAFE === str) ? dquote(str) : str
-
end
-
end
-
-
# Returns true if the string supplied is free from characters not allowed as a TOKEN
-
2
def token_safe?( str )
-
not TOKEN_UNSAFE === str
-
end
-
-
# If the string supplied has TOKEN unsafe characters in it, will return the string quoted
-
# in double quotes, otherwise returns the string unmodified
-
2
def quote_token( str )
-
token_safe?( str ) ? str : dquote(str)
-
end
-
-
# Wraps supplied string in double quotes and applies \-escaping as necessary,
-
# unless it is already wrapped.
-
#
-
# Example:
-
#
-
# string = 'This is a string'
-
# dquote(string) #=> '"This is a string"'
-
#
-
# string = 'This is "a string"'
-
# dquote(string #=> '"This is \"a string\"'
-
2
def dquote( str )
-
'"' + unquote(str).gsub(/[\\"]/n) {|s| '\\' + s } + '"'
-
end
-
-
# Unwraps supplied string from inside double quotes and
-
# removes any \-escaping.
-
#
-
# Example:
-
#
-
# string = '"This is a string"'
-
# unquote(string) #=> 'This is a string'
-
#
-
# string = '"This is \"a string\""'
-
# unqoute(string) #=> 'This is "a string"'
-
2
def unquote( str )
-
if str =~ /^"(.*?)"$/
-
$1.gsub(/\\(.)/, '\1')
-
else
-
str
-
end
-
end
-
-
# Wraps a string in parenthesis and escapes any that are in the string itself.
-
#
-
# Example:
-
#
-
# paren( 'This is a string' ) #=> '(This is a string)'
-
2
def paren( str )
-
RubyVer.paren( str )
-
end
-
-
# Unwraps a string from being wrapped in parenthesis
-
#
-
# Example:
-
#
-
# str = '(This is a string)'
-
# unparen( str ) #=> 'This is a string'
-
2
def unparen( str )
-
match = str.match(/^\((.*?)\)$/)
-
match ? match[1] : str
-
end
-
-
# Wraps a string in angle brackets and escapes any that are in the string itself
-
#
-
# Example:
-
#
-
# bracket( 'This is a string' ) #=> '<This is a string>'
-
2
def bracket( str )
-
RubyVer.bracket( str )
-
end
-
-
# Unwraps a string from being wrapped in parenthesis
-
#
-
# Example:
-
#
-
# str = '<This is a string>'
-
# unbracket( str ) #=> 'This is a string'
-
2
def unbracket( str )
-
match = str.match(/^\<(.*?)\>$/)
-
match ? match[1] : str
-
end
-
-
# Escape parenthesies in a string
-
#
-
# Example:
-
#
-
# str = 'This is (a) string'
-
# escape_paren( str ) #=> 'This is \(a\) string'
-
2
def escape_paren( str )
-
RubyVer.escape_paren( str )
-
end
-
-
2
def uri_escape( str )
-
uri_parser.escape(str)
-
end
-
-
2
def uri_unescape( str )
-
uri_parser.unescape(str)
-
end
-
-
2
def uri_parser
-
@uri_parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
-
end
-
-
# Matches two objects with their to_s values case insensitively
-
#
-
# Example:
-
#
-
# obj2 = "This_is_An_object"
-
# obj1 = :this_IS_an_object
-
# match_to_s( obj1, obj2 ) #=> true
-
2
def match_to_s( obj1, obj2 )
-
obj1.to_s.casecmp(obj2.to_s) == 0
-
end
-
-
# Capitalizes a string that is joined by hyphens correctly.
-
#
-
# Example:
-
#
-
# string = 'resent-from-field'
-
# capitalize_field( string ) #=> 'Resent-From-Field'
-
2
def capitalize_field( str )
-
str.to_s.split("-").map { |v| v.capitalize }.join("-")
-
end
-
-
# Takes an underscored word and turns it into a class name
-
#
-
# Example:
-
#
-
# constantize("hello") #=> "Hello"
-
# constantize("hello-there") #=> "HelloThere"
-
# constantize("hello-there-mate") #=> "HelloThereMate"
-
2
def constantize( str )
-
str.to_s.split(/[-_]/).map { |v| v.capitalize }.to_s
-
end
-
-
# Swaps out all underscores (_) for hyphens (-) good for stringing from symbols
-
# a field name.
-
#
-
# Example:
-
#
-
# string = :resent_from_field
-
# dasherize ( string ) #=> 'resent_from_field'
-
2
def dasherize( str )
-
str.to_s.tr(UNDERSCORE, HYPHEN)
-
end
-
-
# Swaps out all hyphens (-) for underscores (_) good for stringing to symbols
-
# a field name.
-
#
-
# Example:
-
#
-
# string = :resent_from_field
-
# underscoreize ( string ) #=> 'resent_from_field'
-
2
def underscoreize( str )
-
12
str.to_s.downcase.tr(HYPHEN, UNDERSCORE)
-
end
-
-
2
if RUBY_VERSION <= '1.8.6'
-
-
def map_lines( str, &block )
-
results = []
-
str.each_line do |line|
-
results << yield(line)
-
end
-
results
-
end
-
-
def map_with_index( enum, &block )
-
results = []
-
enum.each_with_index do |token, i|
-
results[i] = yield(token, i)
-
end
-
results
-
end
-
-
else
-
-
2
def map_lines( str, &block )
-
str.each_line.map(&block)
-
end
-
-
2
def map_with_index( enum, &block )
-
enum.each_with_index.map(&block)
-
end
-
-
end
-
-
# Test String#encode works correctly with line endings.
-
# Some versions of Ruby (e.g. MRI <1.9, JRuby, Rubinius) line ending
-
# normalization does not work correctly or did not have #encode.
-
2
if ("\r".encode(:universal_newline => true) rescue nil) == LF &&
-
4
(LF.encode(:crlf_newline => true) rescue nil) == CRLF
-
# Using String#encode is better performing than Regexp
-
-
2
def self.to_lf(input)
-
input.kind_of?(String) ? input.to_str.encode(input.encoding, :universal_newline => true) : ''
-
end
-
-
2
def self.to_crlf(input)
-
input.kind_of?(String) ? input.to_str.encode(input.encoding, :universal_newline => true).encode!(input.encoding, :crlf_newline => true) : ''
-
end
-
-
else
-
-
def self.to_lf(input)
-
input.kind_of?(String) ? input.to_str.gsub(/\r\n|\r/, LF) : ''
-
end
-
-
if RUBY_VERSION >= '1.9'
-
# This 1.9 only regex can save a reasonable amount of time (~20%)
-
# by not matching "\r\n" so the string is returned unchanged in
-
# the common case.
-
CRLF_REGEX = Regexp.new("(?<!\r)\n|\r(?!\n)")
-
else
-
CRLF_REGEX = /\n|\r\n|\r/
-
end
-
-
def self.to_crlf(input)
-
input.kind_of?(String) ? input.to_str.gsub(CRLF_REGEX, CRLF) : ''
-
end
-
-
end
-
-
# Returns true if the object is considered blank.
-
# A blank includes things like '', ' ', nil,
-
# and arrays and hashes that have nothing in them.
-
#
-
# This logic is mostly shared with ActiveSupport's blank?
-
2
def self.blank?(value)
-
if value.kind_of?(NilClass)
-
true
-
elsif value.kind_of?(String)
-
value !~ /\S/
-
else
-
value.respond_to?(:empty?) ? value.empty? : !value
-
end
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
2
module Mail
-
2
module VERSION
-
-
2
MAJOR = 2
-
2
MINOR = 6
-
2
PATCH = 4
-
2
BUILD = nil
-
-
2
STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
-
-
2
def self.version
-
STRING
-
end
-
-
end
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
2
module Mail
-
2
class Ruby19
-
2
class StrictCharsetEncoder
-
2
def encode(string, charset)
-
string.force_encoding(Mail::Ruby19.pick_encoding(charset))
-
end
-
end
-
-
2
class BestEffortCharsetEncoder
-
2
def encode(string, charset)
-
string.force_encoding(pick_encoding(charset))
-
end
-
-
2
private
-
-
2
def pick_encoding(charset)
-
charset = case charset
-
when /ansi_x3.110-1983/
-
'ISO-8859-1'
-
when /Windows-?1258/i # Windows-1258 is similar to 1252
-
"Windows-1252"
-
else
-
charset
-
end
-
Mail::Ruby19.pick_encoding(charset)
-
end
-
end
-
-
2
class << self
-
2
attr_accessor :charset_encoder
-
end
-
2
self.charset_encoder = StrictCharsetEncoder.new
-
-
# Escapes any parenthesis in a string that are unescaped this uses
-
# a Ruby 1.9.1 regexp feature of negative look behind
-
2
def Ruby19.escape_paren( str )
-
re = /(?<!\\)([\(\)])/ # Only match unescaped parens
-
str.gsub(re) { |s| '\\' + s }
-
end
-
-
2
def Ruby19.paren( str )
-
str = $1 if str =~ /^\((.*)?\)$/
-
str = escape_paren( str )
-
'(' + str + ')'
-
end
-
-
2
def Ruby19.escape_bracket( str )
-
re = /(?<!\\)([\<\>])/ # Only match unescaped brackets
-
str.gsub(re) { |s| '\\' + s }
-
end
-
-
2
def Ruby19.bracket( str )
-
str = $1 if str =~ /^\<(.*)?\>$/
-
str = escape_bracket( str )
-
'<' + str + '>'
-
end
-
-
2
def Ruby19.decode_base64(str)
-
str.unpack( 'm' ).first
-
end
-
-
2
def Ruby19.encode_base64(str)
-
[str].pack( 'm' )
-
end
-
-
2
def Ruby19.has_constant?(klass, string)
-
klass.const_defined?( string, false )
-
end
-
-
2
def Ruby19.get_constant(klass, string)
-
klass.const_get( string )
-
end
-
-
2
def Ruby19.transcode_charset(str, from_encoding, to_encoding = Encoding::UTF_8)
-
charset_encoder.encode(str.dup, from_encoding).encode(to_encoding, :undef => :replace, :invalid => :replace, :replace => '')
-
end
-
-
2
def Ruby19.b_value_encode(str, encoding = nil)
-
encoding = str.encoding.to_s
-
[Ruby19.encode_base64(str), encoding]
-
end
-
-
2
def Ruby19.b_value_decode(str)
-
match = str.match(/\=\?(.+)?\?[Bb]\?(.*)\?\=/m)
-
if match
-
charset = match[1]
-
str = Ruby19.decode_base64(match[2])
-
str = charset_encoder.encode(str, charset)
-
end
-
decoded = str.encode(Encoding::UTF_8, :invalid => :replace, :replace => "")
-
decoded.valid_encoding? ? decoded : decoded.encode(Encoding::UTF_16LE, :invalid => :replace, :replace => "").encode(Encoding::UTF_8)
-
rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError
-
warn "Encoding conversion failed #{$!}"
-
str.dup.force_encoding(Encoding::UTF_8)
-
end
-
-
2
def Ruby19.q_value_encode(str, encoding = nil)
-
encoding = str.encoding.to_s
-
[Encodings::QuotedPrintable.encode(str), encoding]
-
end
-
-
2
def Ruby19.q_value_decode(str)
-
match = str.match(/\=\?(.+)?\?[Qq]\?(.*)\?\=/m)
-
if match
-
charset = match[1]
-
string = match[2].gsub(/_/, '=20')
-
# Remove trailing = if it exists in a Q encoding
-
string = string.sub(/\=$/, '')
-
str = Encodings::QuotedPrintable.decode(string)
-
str = charset_encoder.encode(str, charset)
-
# We assume that binary strings hold utf-8 directly to work around
-
# jruby/jruby#829 which subtly changes String#encode semantics.
-
str.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::ASCII_8BIT
-
end
-
decoded = str.encode(Encoding::UTF_8, :invalid => :replace, :replace => "")
-
decoded.valid_encoding? ? decoded : decoded.encode(Encoding::UTF_16LE, :invalid => :replace, :replace => "").encode(Encoding::UTF_8)
-
rescue Encoding::UndefinedConversionError, ArgumentError, Encoding::ConverterNotFoundError
-
warn "Encoding conversion failed #{$!}"
-
str.dup.force_encoding(Encoding::UTF_8)
-
end
-
-
2
def Ruby19.param_decode(str, encoding)
-
str = uri_parser.unescape(str)
-
str = charset_encoder.encode(str, encoding) if encoding
-
str
-
end
-
-
2
def Ruby19.param_encode(str)
-
encoding = str.encoding.to_s.downcase
-
language = Configuration.instance.param_encode_language
-
"#{encoding}'#{language}'#{uri_parser.escape(str)}"
-
end
-
-
2
def Ruby19.uri_parser
-
@uri_parser ||= URI::Parser.new
-
end
-
-
# Pick a Ruby encoding corresponding to the message charset. Most
-
# charsets have a Ruby encoding, but some need manual aliasing here.
-
#
-
# TODO: add this as a test somewhere:
-
# Encoding.list.map { |e| [e.to_s.upcase == pick_encoding(e.to_s.downcase.gsub("-", "")), e.to_s] }.select {|a,b| !b}
-
# Encoding.list.map { |e| [e.to_s == pick_encoding(e.to_s), e.to_s] }.select {|a,b| !b}
-
2
def Ruby19.pick_encoding(charset)
-
charset = charset.to_s
-
encoding = case charset.downcase
-
-
# ISO-8859-8-I etc. http://en.wikipedia.org/wiki/ISO-8859-8-I
-
when /^iso[-_]?8859-(\d+)(-i)?$/
-
"ISO-8859-#{$1}"
-
-
# ISO-8859-15, ISO-2022-JP and alike
-
when /^iso[-_]?(\d{4})-?(\w{1,2})$/
-
"ISO-#{$1}-#{$2}"
-
-
# "ISO-2022-JP-KDDI" and alike
-
when /^iso[-_]?(\d{4})-?(\w{1,2})-?(\w*)$/
-
"ISO-#{$1}-#{$2}-#{$3}"
-
-
# UTF-8, UTF-32BE and alike
-
when /^utf[\-_]?(\d{1,2})?(\w{1,2})$/
-
"UTF-#{$1}#{$2}".gsub(/\A(UTF-(?:16|32))\z/, '\\1BE')
-
-
# Windows-1252 and alike
-
when /^windows-?(.*)$/
-
"Windows-#{$1}"
-
-
when '8bit'
-
Encoding::ASCII_8BIT
-
-
# alternatives/misspellings of us-ascii seen in the wild
-
when /^iso[-_]?646(-us)?$/, 'us=ascii'
-
Encoding::ASCII
-
-
# Microsoft-specific alias for MACROMAN
-
when 'macintosh'
-
Encoding::MACROMAN
-
-
# Microsoft-specific alias for CP949 (Korean)
-
when 'ks_c_5601-1987'
-
Encoding::CP949
-
-
# Wrongly written Shift_JIS (Japanese)
-
when 'shift-jis'
-
Encoding::Shift_JIS
-
-
# GB2312 (Chinese charset) is a subset of GB18030 (its replacement)
-
when 'gb2312'
-
Encoding::GB18030
-
-
when 'cp-850'
-
Encoding::CP850
-
-
when 'latin2'
-
Encoding::ISO_8859_2
-
-
else
-
charset
-
end
-
-
convert_to_encoding(encoding)
-
end
-
-
2
class << self
-
2
private
-
-
2
def convert_to_encoding(encoding)
-
if encoding.is_a?(Encoding)
-
encoding
-
else
-
begin
-
Encoding.find(encoding)
-
rescue ArgumentError
-
Encoding::BINARY
-
end
-
end
-
end
-
end
-
end
-
end
-
##
-
2
module MIME
-
end
-
-
# The definition of one MIME content-type.
-
#
-
# == Usage
-
# require 'mime/types'
-
#
-
# plaintext = MIME::Types['text/plain'].first
-
# # returns [text/plain, text/plain]
-
# text = plaintext.first
-
# print text.media_type # => 'text'
-
# print text.sub_type # => 'plain'
-
#
-
# puts text.extensions.join(" ") # => 'asc txt c cc h hh cpp'
-
#
-
# puts text.encoding # => 8bit
-
# puts text.binary? # => false
-
# puts text.ascii? # => true
-
# puts text == 'text/plain' # => true
-
# puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip'
-
#
-
# puts MIME::Types.any? { |type|
-
# type.content_type == 'text/plain'
-
# } # => true
-
# puts MIME::Types.all?(&:registered?)
-
# # => false
-
2
class MIME::Type
-
# Reflects a MIME content-type specification that is not correctly
-
# formatted (it isn't +type+/+subtype+).
-
2
class InvalidContentType < ArgumentError
-
# :stopdoc:
-
2
def initialize(type_string)
-
@type_string = type_string
-
end
-
-
2
def to_s
-
"Invalid Content-Type #{@type_string.inspect}"
-
end
-
# :startdoc:
-
end
-
-
# Reflects an unsupported MIME encoding.
-
2
class InvalidEncoding < ArgumentError
-
# :stopdoc:
-
2
def initialize(encoding)
-
@encoding = encoding
-
end
-
-
2
def to_s
-
"Invalid Encoding #{@encoding.inspect}"
-
end
-
# :startdoc:
-
end
-
-
# The released version of the mime-types library.
-
2
VERSION = '3.1'
-
-
2
include Comparable
-
-
# :stopdoc:
-
# TODO verify mime-type character restrictions; I am pretty sure that this is
-
# too wide open.
-
2
MEDIA_TYPE_RE = %r{([-\w.+]+)/([-\w.+]*)}
-
2
I18N_RE = %r{[^[:alnum:]]}
-
2
BINARY_ENCODINGS = %w(base64 8bit)
-
2
ASCII_ENCODINGS = %w(7bit quoted-printable)
-
# :startdoc:
-
-
2
private_constant :MEDIA_TYPE_RE, :I18N_RE, :BINARY_ENCODINGS,
-
:ASCII_ENCODINGS
-
-
# Builds a MIME::Type object from the +content_type+, a MIME Content Type
-
# value (e.g., 'text/plain' or 'applicaton/x-eruby'). The constructed object
-
# is yielded to an optional block for additional configuration, such as
-
# associating extensions and encoding information.
-
#
-
# * When provided a Hash or a MIME::Type, the MIME::Type will be
-
# constructed with #init_with.
-
# * When provided an Array, the MIME::Type will be constructed using
-
# the first element as the content type and the remaining flattened
-
# elements as extensions.
-
# * Otherwise, the content_type will be used as a string.
-
#
-
# Yields the newly constructed +self+ object.
-
2
def initialize(content_type) # :yields self:
-
@friendly = {}
-
@obsolete = @registered = false
-
@preferred_extension = @docs = @use_instead = nil
-
self.extensions = []
-
-
case content_type
-
when Hash
-
init_with(content_type)
-
when Array
-
self.content_type = content_type.shift
-
self.extensions = content_type.flatten
-
when MIME::Type
-
init_with(content_type.to_h)
-
else
-
self.content_type = content_type
-
end
-
-
self.encoding ||= :default
-
self.xrefs ||= {}
-
-
yield self if block_given?
-
end
-
-
# Indicates that a MIME type is like another type. This differs from
-
# <tt>==</tt> because <tt>x-</tt> prefixes are removed for this comparison.
-
2
def like?(other)
-
other = if other.respond_to?(:simplified)
-
MIME::Type.simplified(other.simplified, remove_x_prefix: true)
-
else
-
MIME::Type.simplified(other.to_s, remove_x_prefix: true)
-
end
-
MIME::Type.simplified(simplified, remove_x_prefix: true) == other
-
end
-
-
# Compares the +other+ MIME::Type against the exact content type or the
-
# simplified type (the simplified type will be used if comparing against
-
# something that can be treated as a String with #to_s). In comparisons, this
-
# is done against the lowercase version of the MIME::Type.
-
2
def <=>(other)
-
7856
if other.nil?
-
-1
-
7856
elsif other.respond_to?(:simplified)
-
simplified <=> other.simplified
-
else
-
7856
simplified <=> MIME::Type.simplified(other.to_s)
-
end
-
end
-
-
# Compares the +other+ MIME::Type based on how reliable it is before doing a
-
# normal <=> comparison. Used by MIME::Types#[] to sort types. The
-
# comparisons involved are:
-
#
-
# 1. self.simplified <=> other.simplified (ensures that we
-
# don't try to compare different types)
-
# 2. IANA-registered definitions < other definitions.
-
# 3. Complete definitions < incomplete definitions.
-
# 4. Current definitions < obsolete definitions.
-
# 5. Obselete with use-instead names < obsolete without.
-
# 6. Obsolete use-instead definitions are compared.
-
#
-
# While this method is public, its use is strongly discouraged by consumers
-
# of mime-types. In mime-types 3, this method is likely to see substantial
-
# revision and simplification to ensure current registered content types sort
-
# before unregistered or obsolete content types.
-
2
def priority_compare(other)
-
pc = simplified <=> other.simplified
-
if pc.zero?
-
pc = if (reg = registered?) != other.registered?
-
reg ? -1 : 1 # registered < unregistered
-
elsif (comp = complete?) != other.complete?
-
comp ? -1 : 1 # complete < incomplete
-
elsif (obs = obsolete?) != other.obsolete?
-
obs ? 1 : -1 # current < obsolete
-
elsif obs and ((ui = use_instead) != (oui = other.use_instead))
-
if ui.nil?
-
1
-
elsif oui.nil?
-
-1
-
else
-
ui <=> oui
-
end
-
else
-
0
-
end
-
end
-
-
pc
-
end
-
-
# Returns +true+ if the +other+ object is a MIME::Type and the content types
-
# match.
-
2
def eql?(other)
-
other.kind_of?(MIME::Type) and self == other
-
end
-
-
# Returns the whole MIME content-type string.
-
#
-
# The content type is a presentation value from the MIME type registry and
-
# should not be used for comparison. The case of the content type is
-
# preserved, and extension markers (<tt>x-</tt>) are kept.
-
#
-
# text/plain => text/plain
-
# x-chemical/x-pdb => x-chemical/x-pdb
-
# audio/QCELP => audio/QCELP
-
2
attr_reader :content_type
-
# A simplified form of the MIME content-type string, suitable for
-
# case-insensitive comparison, with any extension markers (<tt>x-</tt)
-
# removed and converted to lowercase.
-
#
-
# text/plain => text/plain
-
# x-chemical/x-pdb => x-chemical/x-pdb
-
# audio/QCELP => audio/qcelp
-
2
attr_reader :simplified
-
# Returns the media type of the simplified MIME::Type.
-
#
-
# text/plain => text
-
# x-chemical/x-pdb => x-chemical
-
# audio/QCELP => audio
-
2
attr_reader :media_type
-
# Returns the media type of the unmodified MIME::Type.
-
#
-
# text/plain => text
-
# x-chemical/x-pdb => x-chemical
-
# audio/QCELP => audio
-
2
attr_reader :raw_media_type
-
# Returns the sub-type of the simplified MIME::Type.
-
#
-
# text/plain => plain
-
# x-chemical/x-pdb => pdb
-
# audio/QCELP => QCELP
-
2
attr_reader :sub_type
-
# Returns the media type of the unmodified MIME::Type.
-
#
-
# text/plain => plain
-
# x-chemical/x-pdb => x-pdb
-
# audio/QCELP => qcelp
-
2
attr_reader :raw_sub_type
-
-
##
-
# The list of extensions which are known to be used for this MIME::Type.
-
# Non-array values will be coerced into an array with #to_a. Array values
-
# will be flattened, +nil+ values removed, and made unique.
-
#
-
# :attr_accessor: extensions
-
2
def extensions
-
3928
@extensions.to_a
-
end
-
-
##
-
2
def extensions=(value) # :nodoc:
-
3928
@extensions = Set[*Array(value).flatten.compact].freeze
-
3928
MIME::Types.send(:reindex_extensions, self)
-
end
-
-
# Merge the +extensions+ provided into this MIME::Type. The extensions added
-
# will be merged uniquely.
-
2
def add_extensions(*extensions)
-
self.extensions += extensions
-
end
-
-
##
-
# The preferred extension for this MIME type. If one is not set and there are
-
# exceptions defined, the first extension will be used.
-
#
-
# When setting #preferred_extensions, if #extensions does not contain this
-
# extension, this will be added to #xtensions.
-
#
-
# :attr_accessor: preferred_extension
-
-
##
-
2
def preferred_extension
-
@preferred_extension || extensions.first
-
end
-
-
##
-
2
def preferred_extension=(value) # :nodoc:
-
add_extensions(value) if value
-
@preferred_extension = value
-
end
-
-
##
-
# The encoding (+7bit+, +8bit+, <tt>quoted-printable</tt>, or +base64+)
-
# required to transport the data of this content type safely across a
-
# network, which roughly corresponds to Content-Transfer-Encoding. A value of
-
# +nil+ or <tt>:default</tt> will reset the #encoding to the
-
# #default_encoding for the MIME::Type. Raises ArgumentError if the encoding
-
# provided is invalid.
-
#
-
# If the encoding is not provided on construction, this will be either
-
# 'quoted-printable' (for text/* media types) and 'base64' for eveything
-
# else.
-
#
-
# :attr_accessor: encoding
-
-
##
-
2
attr_reader :encoding
-
-
##
-
2
def encoding=(enc) # :nodoc:
-
if enc.nil? or enc == :default
-
@encoding = default_encoding
-
elsif BINARY_ENCODINGS.include?(enc) or ASCII_ENCODINGS.include?(enc)
-
@encoding = enc
-
else
-
fail InvalidEncoding, enc
-
end
-
end
-
-
# Returns the default encoding for the MIME::Type based on the media type.
-
2
def default_encoding
-
(@media_type == 'text') ? 'quoted-printable' : 'base64'
-
end
-
-
##
-
# Returns the media type or types that should be used instead of this media
-
# type, if it is obsolete. If there is no replacement media type, or it is
-
# not obsolete, +nil+ will be returned.
-
#
-
# :attr_accessor: use_instead
-
-
##
-
2
def use_instead
-
obsolete? ? @use_instead : nil
-
end
-
-
##
-
2
attr_writer :use_instead
-
-
# Returns +true+ if the media type is obsolete.
-
2
attr_accessor :obsolete
-
2
alias_method :obsolete?, :obsolete
-
-
# The documentation for this MIME::Type.
-
2
attr_accessor :docs
-
-
# A friendly short description for this MIME::Type.
-
#
-
# call-seq:
-
# text_plain.friendly # => "Text File"
-
# text_plain.friendly('en') # => "Text File"
-
2
def friendly(lang = 'en'.freeze)
-
@friendly ||= {}
-
-
case lang
-
when String, Symbol
-
@friendly[lang.to_s]
-
when Array
-
@friendly.update(Hash[*lang])
-
when Hash
-
@friendly.update(lang)
-
else
-
fail ArgumentError,
-
"Expected a language or translation set, not #{lang.inspect}"
-
end
-
end
-
-
# A key suitable for use as a lookup key for translations, such as with
-
# the I18n library.
-
#
-
# call-seq:
-
# text_plain.i18n_key # => "text.plain"
-
# 3gpp_xml.i18n_key # => "application.vnd-3gpp-bsf-xml"
-
# # from application/vnd.3gpp.bsf+xml
-
# x_msword.i18n_key # => "application.word"
-
# # from application/x-msword
-
2
attr_reader :i18n_key
-
-
##
-
# The cross-references list for this MIME::Type.
-
#
-
# :attr_accessor: xrefs
-
-
##
-
2
attr_reader :xrefs
-
-
##
-
2
def xrefs=(x) # :nodoc:
-
MIME::Types::Container.new.merge(x).tap do |xr|
-
xr.each do |k, v|
-
xr[k] = Set[*v] unless v.kind_of? Set
-
end
-
-
@xrefs = xr
-
end
-
end
-
-
# The decoded cross-reference URL list for this MIME::Type.
-
2
def xref_urls
-
xrefs.flat_map { |type, values|
-
name = :"xref_url_for_#{type.tr('-', '_')}"
-
respond_to?(name, true) and xref_map(values, name) or values.to_a
-
}
-
end
-
-
# Indicates whether the MIME type has been registered with IANA.
-
2
attr_accessor :registered
-
2
alias_method :registered?, :registered
-
-
# MIME types can be specified to be sent across a network in particular
-
# formats. This method returns +true+ when the MIME::Type encoding is set
-
# to <tt>base64</tt>.
-
2
def binary?
-
BINARY_ENCODINGS.include?(encoding)
-
end
-
-
# MIME types can be specified to be sent across a network in particular
-
# formats. This method returns +false+ when the MIME::Type encoding is
-
# set to <tt>base64</tt>.
-
2
def ascii?
-
ASCII_ENCODINGS.include?(encoding)
-
end
-
-
# Indicateswhether the MIME type is declared as a signature type.
-
2
attr_accessor :signature
-
2
alias_method :signature?, :signature
-
-
# Returns +true+ if the MIME::Type specifies an extension list,
-
# indicating that it is a complete MIME::Type.
-
2
def complete?
-
!@extensions.empty?
-
end
-
-
# Returns the MIME::Type as a string.
-
2
def to_s
-
content_type
-
end
-
-
# Returns the MIME::Type as a string for implicit conversions. This allows
-
# MIME::Type objects to appear on either side of a comparison.
-
#
-
# 'text/plain' == MIME::Type.new('text/plain')
-
2
def to_str
-
content_type
-
end
-
-
# Converts the MIME::Type to a JSON string.
-
2
def to_json(*args)
-
require 'json'
-
to_h.to_json(*args)
-
end
-
-
# Converts the MIME::Type to a hash. The output of this method can also be
-
# used to initialize a MIME::Type.
-
2
def to_h
-
encode_with({})
-
end
-
-
# Populates the +coder+ with attributes about this record for
-
# serialization. The structure of +coder+ should match the structure used
-
# with #init_with.
-
#
-
# This method should be considered a private implementation detail.
-
2
def encode_with(coder)
-
coder['content-type'] = @content_type
-
coder['docs'] = @docs unless @docs.nil? or @docs.empty?
-
unless @friendly.nil? or @friendly.empty?
-
coder['friendly'] = @friendly
-
end
-
coder['encoding'] = @encoding
-
coder['extensions'] = @extensions.to_a unless @extensions.empty?
-
coder['preferred-extension'] = @preferred_extension if @preferred_extension
-
if obsolete?
-
coder['obsolete'] = obsolete?
-
coder['use-instead'] = use_instead if use_instead
-
end
-
unless xrefs.empty?
-
{}.tap do |hash|
-
xrefs.each do |k, v|
-
hash[k] = v.sort.to_a
-
end
-
coder['xrefs'] = hash
-
end
-
end
-
coder['registered'] = registered?
-
coder['signature'] = signature? if signature?
-
coder
-
end
-
-
# Initialize an empty object from +coder+, which must contain the
-
# attributes necessary for initializing an empty object.
-
#
-
# This method should be considered a private implementation detail.
-
2
def init_with(coder)
-
self.content_type = coder['content-type']
-
self.docs = coder['docs'] || ''
-
self.encoding = coder['encoding']
-
self.extensions = coder['extensions'] || []
-
self.preferred_extension = coder['preferred-extension']
-
self.obsolete = coder['obsolete'] || false
-
self.registered = coder['registered'] || false
-
self.signature = coder['signature']
-
self.xrefs = coder['xrefs'] || {}
-
self.use_instead = coder['use-instead']
-
-
friendly(coder['friendly'] || {})
-
end
-
-
2
def inspect # :nodoc:
-
# We are intentionally lying here because MIME::Type::Columnar is an
-
# implementation detail.
-
"#<MIME::Type: #{self}>"
-
end
-
-
2
class << self
-
# MIME media types are case-insensitive, but are typically presented in a
-
# case-preserving format in the type registry. This method converts
-
# +content_type+ to lowercase.
-
#
-
# In previous versions of mime-types, this would also remove any extension
-
# prefix (<tt>x-</tt>). This is no longer default behaviour, but may be
-
# provided by providing a truth value to +remove_x_prefix+.
-
2
def simplified(content_type, remove_x_prefix: false)
-
11784
simplify_matchdata(match(content_type), remove_x_prefix)
-
end
-
-
# Converts a provided +content_type+ into a translation key suitable for
-
# use with the I18n library.
-
2
def i18n_key(content_type)
-
3928
simplify_matchdata(match(content_type), joiner: '.') { |e|
-
7856
e.gsub!(I18N_RE, '-'.freeze)
-
}
-
end
-
-
# Return a +MatchData+ object of the +content_type+ against pattern of
-
# media types.
-
2
def match(content_type)
-
15712
case content_type
-
when MatchData
-
7856
content_type
-
else
-
7856
MEDIA_TYPE_RE.match(content_type)
-
end
-
end
-
-
2
private
-
-
2
def simplify_matchdata(matchdata, remove_x = false, joiner: '/'.freeze)
-
15712
return nil unless matchdata
-
-
matchdata.captures.map { |e|
-
15712
e.downcase!
-
15712
e.sub!(%r{^x-}, ''.freeze) if remove_x
-
15712
yield e if block_given?
-
15712
e
-
7856
}.join(joiner)
-
end
-
end
-
-
2
private
-
-
2
def content_type=(type_string)
-
3928
match = MEDIA_TYPE_RE.match(type_string)
-
3928
fail InvalidContentType, type_string if match.nil?
-
-
3928
@content_type = type_string
-
3928
@raw_media_type, @raw_sub_type = match.captures
-
3928
@simplified = MIME::Type.simplified(match)
-
3928
@i18n_key = MIME::Type.i18n_key(match)
-
3928
@media_type, @sub_type = MEDIA_TYPE_RE.match(@simplified).captures
-
end
-
-
2
def xref_map(values, helper)
-
values.map { |value| send(helper, value) }
-
end
-
-
2
def xref_url_for_rfc(value)
-
'http://www.iana.org/go/%s'.freeze % value
-
end
-
-
2
def xref_url_for_draft(value)
-
'http://www.iana.org/go/%s'.freeze % value.sub(/\ARFC/, 'draft')
-
end
-
-
2
def xref_url_for_rfc_errata(value)
-
'http://www.rfc-editor.org/errata_search.php?eid=%s'.freeze % value
-
end
-
-
2
def xref_url_for_person(value)
-
'http://www.iana.org/assignments/media-types/media-types.xhtml#%s'.freeze %
-
value
-
end
-
-
2
def xref_url_for_template(value)
-
'http://www.iana.org/assignments/media-types/%s'.freeze % value
-
end
-
end
-
2
require 'mime/type'
-
-
# A version of MIME::Type that works hand-in-hand with a MIME::Types::Columnar
-
# container to load data by columns.
-
#
-
# When a field is has not yet been loaded, that data will be loaded for all
-
# types in the container before forwarding the message to MIME::Type.
-
#
-
# More information can be found in MIME::Types::Columnar.
-
#
-
# MIME::Type::Columnar is *not* intended to be created except by
-
# MIME::Types::Columnar containers.
-
2
class MIME::Type::Columnar < MIME::Type
-
2
def initialize(container, content_type, extensions) # :nodoc:
-
3928
@container = container
-
3928
self.content_type = content_type
-
3928
self.extensions = extensions
-
end
-
-
2
def self.column(*methods, file: nil) # :nodoc:
-
14
file = methods.first unless file
-
-
14
file_method = :"load_#{file}"
-
14
methods.each do |m|
-
42
define_method m do |*args|
-
@container.send(file_method)
-
super(*args)
-
end
-
end
-
end
-
-
2
column :friendly
-
2
column :encoding, :encoding=
-
2
column :docs, :docs=
-
2
column :preferred_extension, :preferred_extension=
-
2
column :obsolete, :obsolete=, :obsolete?, :registered, :registered=,
-
:registered?, :signature, :signature=, :signature?, file: 'flags'
-
2
column :xrefs, :xrefs=, :xref_urls
-
2
column :use_instead, :use_instead=
-
-
2
def encode_with(coder) # :nodoc:
-
@container.send(:load_friendly)
-
@container.send(:load_encoding)
-
@container.send(:load_docs)
-
@container.send(:load_flags)
-
@container.send(:load_use_instead)
-
@container.send(:load_xrefs)
-
@container.send(:load_preferred_extension)
-
super
-
end
-
-
2
class << self
-
2
undef column
-
end
-
end
-
##
-
2
module MIME
-
##
-
2
class Types
-
end
-
end
-
-
2
require 'mime/type'
-
-
# MIME::Types is a registry of MIME types. It is both a class (created with
-
# MIME::Types.new) and a default registry (loaded automatically or through
-
# interactions with MIME::Types.[] and MIME::Types.type_for).
-
#
-
# == The Default mime-types Registry
-
#
-
# The default mime-types registry is loaded automatically when the library
-
# is required (<tt>require 'mime/types'</tt>), but it may be lazily loaded
-
# (loaded on first use) with the use of the environment variable
-
# +RUBY_MIME_TYPES_LAZY_LOAD+ having any value other than +false+. The
-
# initial startup is about 14× faster (~10 ms vs ~140 ms), but the
-
# registry will be loaded at some point in the future.
-
#
-
# The default mime-types registry can also be loaded from a Marshal cache
-
# file specific to the version of MIME::Types being loaded. This will be
-
# handled automatically with the use of a file referred to in the
-
# environment variable +RUBY_MIME_TYPES_CACHE+. MIME::Types will attempt to
-
# load the registry from this cache file (MIME::Type::Cache.load); if it
-
# cannot be loaded (because the file does not exist, there is an error, or
-
# the data is for a different version of mime-types), the default registry
-
# will be loaded from the normal JSON version and then the cache file will
-
# be *written* to the location indicated by +RUBY_MIME_TYPES_CACHE+. Cache
-
# file loads just over 4½× faster (~30 ms vs ~140 ms).
-
# loads.
-
#
-
# Notes:
-
# * The loading of the default registry is *not* atomic; when using a
-
# multi-threaded environment, it is recommended that lazy loading is not
-
# used and mime-types is loaded as early as possible.
-
# * Cache files should be specified per application in a multiprocess
-
# environment and should be initialized during deployment or before
-
# forking to minimize the chance that the multiple processes will be
-
# trying to write to the same cache file at the same time, or that two
-
# applications that are on different versions of mime-types would be
-
# thrashing the cache.
-
# * Unless cache files are preinitialized, the application using the
-
# mime-types cache file must have read/write permission to the cache file.
-
#
-
# == Usage
-
# require 'mime/types'
-
#
-
# plaintext = MIME::Types['text/plain']
-
# print plaintext.media_type # => 'text'
-
# print plaintext.sub_type # => 'plain'
-
#
-
# puts plaintext.extensions.join(" ") # => 'asc txt c cc h hh cpp'
-
#
-
# puts plaintext.encoding # => 8bit
-
# puts plaintext.binary? # => false
-
# puts plaintext.ascii? # => true
-
# puts plaintext.obsolete? # => false
-
# puts plaintext.registered? # => true
-
# puts plaintext == 'text/plain' # => true
-
# puts MIME::Type.simplified('x-appl/x-zip') # => 'appl/zip'
-
#
-
2
class MIME::Types
-
# The release version of Ruby MIME::Types
-
2
VERSION = MIME::Type::VERSION
-
-
2
include Enumerable
-
-
# Creates a new MIME::Types registry.
-
2
def initialize
-
2
@type_variants = Container.new
-
2
@extension_index = Container.new
-
end
-
-
# Returns the number of known type variants.
-
2
def count
-
@type_variants.values.inject(0) { |a, e| a + e.size }
-
end
-
-
2
def inspect # :nodoc:
-
"#<#{self.class}: #{count} variants, #{@extension_index.count} extensions>"
-
end
-
-
# Iterates through the type variants.
-
2
def each
-
if block_given?
-
@type_variants.each_value { |tv| tv.each { |t| yield t } }
-
else
-
enum_for(:each)
-
end
-
end
-
-
2
@__types__ = nil
-
-
# Returns a list of MIME::Type objects, which may be empty. The optional
-
# flag parameters are <tt>:complete</tt> (finds only complete MIME::Type
-
# objects) and <tt>:registered</tt> (finds only MIME::Types that are
-
# registered). It is possible for multiple matches to be returned for
-
# either type (in the example below, 'text/plain' returns two values --
-
# one for the general case, and one for VMS systems).
-
#
-
# puts "\nMIME::Types['text/plain']"
-
# MIME::Types['text/plain'].each { |t| puts t.to_a.join(", ") }
-
#
-
# puts "\nMIME::Types[/^image/, complete: true]"
-
# MIME::Types[/^image/, :complete => true].each do |t|
-
# puts t.to_a.join(", ")
-
# end
-
#
-
# If multiple type definitions are returned, returns them sorted as
-
# follows:
-
# 1. Complete definitions sort before incomplete ones;
-
# 2. IANA-registered definitions sort before LTSW-recorded
-
# definitions.
-
# 3. Current definitions sort before obsolete ones;
-
# 4. Obsolete definitions with use-instead clauses sort before those
-
# without;
-
# 5. Obsolete definitions use-instead clauses are compared.
-
# 6. Sort on name.
-
2
def [](type_id, complete: false, registered: false)
-
matches = case type_id
-
when MIME::Type
-
@type_variants[type_id.simplified]
-
when Regexp
-
match(type_id)
-
else
-
@type_variants[MIME::Type.simplified(type_id)]
-
end
-
-
prune_matches(matches, complete, registered).sort { |a, b|
-
a.priority_compare(b)
-
}
-
end
-
-
# Return the list of MIME::Types which belongs to the file based on its
-
# filename extension. If there is no extension, the filename will be used
-
# as the matching criteria on its own.
-
#
-
# This will always return a merged, flatten, priority sorted, unique array.
-
#
-
# puts MIME::Types.type_for('citydesk.xml')
-
# => [application/xml, text/xml]
-
# puts MIME::Types.type_for('citydesk.gif')
-
# => [image/gif]
-
# puts MIME::Types.type_for(%w(citydesk.xml citydesk.gif))
-
# => [application/xml, image/gif, text/xml]
-
2
def type_for(filename)
-
Array(filename).flat_map { |fn|
-
@extension_index[fn.chomp.downcase[/\.?([^.]*?)$/, 1]]
-
}.compact.inject(:+).sort { |a, b|
-
a.priority_compare(b)
-
}
-
end
-
2
alias_method :of, :type_for
-
-
# Add one or more MIME::Type objects to the set of known types. If the
-
# type is already known, a warning will be displayed.
-
#
-
# The last parameter may be the value <tt>:silent</tt> or +true+ which
-
# will suppress duplicate MIME type warnings.
-
2
def add(*types)
-
3928
quiet = ((types.last == :silent) or (types.last == true))
-
-
3928
types.each do |mime_type|
-
3928
case mime_type
-
when true, false, nil, Symbol
-
nil
-
when MIME::Types
-
variants = mime_type.instance_variable_get(:@type_variants)
-
add(*variants.values.inject(:+).to_a, quiet)
-
when Array
-
add(*mime_type, quiet)
-
else
-
3928
add_type(mime_type, quiet)
-
end
-
end
-
end
-
-
# Add a single MIME::Type object to the set of known types. If the +type+ is
-
# already known, a warning will be displayed. The +quiet+ parameter may be a
-
# truthy value to suppress that warning.
-
2
def add_type(type, quiet = false)
-
3928
if !quiet and @type_variants[type.simplified].include?(type)
-
MIME::Types.logger.warn <<-warning
-
Type #{type} is already registered as a variant of #{type.simplified}.
-
warning
-
end
-
-
3928
add_type_variant!(type)
-
3928
index_extensions!(type)
-
end
-
-
2
private
-
-
2
def add_type_variant!(mime_type)
-
3928
@type_variants[mime_type.simplified] << mime_type
-
end
-
-
2
def reindex_extensions!(mime_type)
-
3928
return unless @type_variants[mime_type.simplified].include?(mime_type)
-
index_extensions!(mime_type)
-
end
-
-
2
def index_extensions!(mime_type)
-
6560
mime_type.extensions.each { |ext| @extension_index[ext] << mime_type }
-
end
-
-
2
def prune_matches(matches, complete, registered)
-
matches.delete_if { |e| !e.complete? } if complete
-
matches.delete_if { |e| !e.registered? } if registered
-
matches
-
end
-
-
2
def match(pattern)
-
@type_variants.select { |k, _|
-
k =~ pattern
-
}.values.inject(:+)
-
end
-
end
-
-
2
require 'mime/types/cache'
-
2
require 'mime/types/container'
-
2
require 'mime/types/loader'
-
2
require 'mime/types/logger'
-
2
require 'mime/types/_columnar'
-
2
require 'mime/types/registry'
-
2
require 'mime/type/columnar'
-
-
# MIME::Types::Columnar is used to extend a MIME::Types container to load data
-
# by columns instead of from JSON or YAML. Column loads of MIME types loaded
-
# through the columnar store are synchronized with a Mutex.
-
#
-
# MIME::Types::Columnar is not intended to be used directly, but will be added
-
# to an instance of MIME::Types when it is loaded with
-
# MIME::Types::Loader#load_columnar.
-
2
module MIME::Types::Columnar
-
2
LOAD_MUTEX = Mutex.new # :nodoc:
-
-
2
def self.extended(obj) # :nodoc:
-
2
super
-
2
obj.instance_variable_set(:@__mime_data__, [])
-
2
obj.instance_variable_set(:@__files__, Set.new)
-
end
-
-
# Load the first column data file (type and extensions).
-
2
def load_base_data(path) #:nodoc:
-
2
@__root__ = path
-
-
2
each_file_line('content_type', false) do |line|
-
3928
line = line.split
-
3928
content_type = line.shift
-
3928
extensions = line
-
# content_type, *extensions = line.split
-
-
3928
type = MIME::Type::Columnar.new(self, content_type, extensions)
-
3928
@__mime_data__ << type
-
3928
add(type)
-
end
-
-
2
self
-
end
-
-
2
private
-
-
2
def each_file_line(name, lookup = true)
-
2
LOAD_MUTEX.synchronize do
-
2
next if @__files__.include?(name)
-
-
2
i = -1
-
2
column = File.join(@__root__, "mime.#{name}.column")
-
-
2
IO.readlines(column, encoding: 'UTF-8'.freeze).each do |line|
-
3928
line.chomp!
-
-
3928
if lookup
-
type = @__mime_data__[i += 1] or next
-
yield type, line
-
else
-
3928
yield line
-
end
-
end
-
-
2
@__files__ << name
-
end
-
end
-
-
2
def load_encoding
-
each_file_line('encoding') do |type, line|
-
pool ||= {}
-
line.freeze
-
type.instance_variable_set(:@encoding, (pool[line] ||= line))
-
end
-
end
-
-
2
def load_docs
-
each_file_line('docs') do |type, line|
-
type.instance_variable_set(:@docs, opt(line))
-
end
-
end
-
-
2
def load_preferred_extension
-
each_file_line('pext') do |type, line|
-
type.instance_variable_set(:@preferred_extension, opt(line))
-
end
-
end
-
-
2
def load_flags
-
each_file_line('flags') do |type, line|
-
line = line.split
-
type.instance_variable_set(:@obsolete, flag(line.shift))
-
type.instance_variable_set(:@registered, flag(line.shift))
-
type.instance_variable_set(:@signature, flag(line.shift))
-
end
-
end
-
-
2
def load_xrefs
-
each_file_line('xrefs') { |type, line|
-
type.instance_variable_set(:@xrefs, dict(line, array: true))
-
}
-
end
-
-
2
def load_friendly
-
each_file_line('friendly') { |type, line|
-
type.instance_variable_set(:@friendly, dict(line))
-
}
-
end
-
-
2
def load_use_instead
-
each_file_line('use_instead') do |type, line|
-
type.instance_variable_set(:@use_instead, opt(line))
-
end
-
end
-
-
2
def dict(line, array: false)
-
if line == '-'.freeze
-
{}
-
else
-
line.split('|'.freeze).each_with_object({}) { |l, h|
-
k, v = l.split('^'.freeze)
-
v = nil if v.empty?
-
h[k] = array ? Array(v) : v
-
}
-
end
-
end
-
-
2
def arr(line)
-
if line == '-'.freeze
-
[]
-
else
-
line.split('|'.freeze).flatten.compact.uniq
-
end
-
end
-
-
2
def opt(line)
-
line unless line == '-'.freeze
-
end
-
-
2
def flag(line)
-
line == '1'.freeze ? true : false
-
end
-
end
-
2
MIME::Types::Cache = Struct.new(:version, :data) # :nodoc:
-
-
# Caching of MIME::Types registries is advisable if you will be loading
-
# the default registry relatively frequently. With the class methods on
-
# MIME::Types::Cache, any MIME::Types registry can be marshaled quickly
-
# and easily.
-
#
-
# The cache is invalidated on a per-data-version basis; a cache file for
-
# version 3.2015.1118 will not be reused with version 3.2015.1201.
-
2
class << MIME::Types::Cache
-
# Attempts to load the cache from the file provided as a parameter or in
-
# the environment variable +RUBY_MIME_TYPES_CACHE+. Returns +nil+ if the
-
# file does not exist, if the file cannot be loaded, or if the data in
-
# the cache version is different than this version.
-
2
def load(cache_file = nil)
-
2
cache_file ||= ENV['RUBY_MIME_TYPES_CACHE']
-
2
return nil unless cache_file and File.exist?(cache_file)
-
-
cache = Marshal.load(File.binread(cache_file))
-
if cache.version == MIME::Types::Data::VERSION
-
Marshal.load(cache.data)
-
else
-
MIME::Types.logger.warn <<-warning.chomp
-
Could not load MIME::Types cache: invalid version
-
warning
-
nil
-
end
-
rescue => e
-
MIME::Types.logger.warn <<-warning.chomp
-
Could not load MIME::Types cache: #{e}
-
warning
-
return nil
-
end
-
-
# Attempts to save the types provided to the cache file provided.
-
#
-
# If +types+ is not provided or is +nil+, the cache will contain the
-
# current MIME::Types default registry.
-
#
-
# If +cache_file+ is not provided or is +nil+, the cache will be written
-
# to the file specified in the environment variable
-
# +RUBY_MIME_TYPES_CACHE+. If there is no cache file specified either
-
# directly or through the environment, this method will return +nil+
-
2
def save(types = nil, cache_file = nil)
-
2
cache_file ||= ENV['RUBY_MIME_TYPES_CACHE']
-
2
return nil unless cache_file
-
-
types ||= MIME::Types.send(:__types__)
-
-
File.open(cache_file, 'wb') do |f|
-
f.write(
-
Marshal.dump(new(MIME::Types::Data::VERSION, Marshal.dump(types)))
-
)
-
end
-
end
-
end
-
2
require 'mime/types'
-
2
require 'set'
-
-
# MIME::Types requires a container Hash with a default values for keys
-
# resulting in an empty array (<tt>[]</tt>), but this cannot be dumped through
-
# Marshal because of the presence of that default Proc. This class exists
-
# solely to satisfy that need.
-
2
class MIME::Types::Container < Hash # :nodoc:
-
2
def initialize
-
4
super
-
6256
self.default_proc = ->(h, k) { h[k] = Set.new }
-
end
-
-
2
def marshal_dump
-
{}.merge(self)
-
end
-
-
2
def marshal_load(hash)
-
self.default_proc = ->(h, k) { h[k] = Set.new }
-
merge!(hash)
-
end
-
-
2
def encode_with(coder)
-
each { |k, v| coder[k] = v.to_a }
-
end
-
-
2
def init_with(coder)
-
self.default_proc = ->(h, k) { h[k] = Set.new }
-
coder.map.each { |k, v| self[k] = Set[*v] }
-
end
-
end
-
# -*- ruby encoding: utf-8 -*-
-
-
##
-
2
module MIME; end
-
##
-
2
class MIME::Types; end
-
-
2
require 'mime/types/data'
-
-
# This class is responsible for initializing the MIME::Types registry from
-
# the data files supplied with the mime-types library.
-
#
-
# The Loader will use one of the following paths:
-
# 1. The +path+ provided in its constructor argument;
-
# 2. The value of ENV['RUBY_MIME_TYPES_DATA']; or
-
# 3. The value of MIME::Types::Data::PATH.
-
#
-
# When #load is called, the +path+ will be searched recursively for all YAML
-
# (.yml or .yaml) files. By convention, there is one file for each media
-
# type (application.yml, audio.yml, etc.), but this is not required.
-
2
class MIME::Types::Loader
-
# The path that will be read for the MIME::Types files.
-
2
attr_reader :path
-
# The MIME::Types container instance that will be loaded. If not provided
-
# at initialization, a new MIME::Types instance will be constructed.
-
2
attr_reader :container
-
-
# Creates a Loader object that can be used to load MIME::Types registries
-
# into memory, using YAML, JSON, or Columnar registry format loaders.
-
2
def initialize(path = nil, container = nil)
-
2
path = path || ENV['RUBY_MIME_TYPES_DATA'] || MIME::Types::Data::PATH
-
2
@container = container || MIME::Types.new
-
2
@path = File.expand_path(path)
-
# begin
-
# require 'mime/lazy_types'
-
# @container.extend(MIME::LazyTypes)
-
# end
-
end
-
-
# Loads a MIME::Types registry from YAML files (<tt>*.yml</tt> or
-
# <tt>*.yaml</tt>) recursively found in +path+.
-
#
-
# It is expected that the YAML objects contained within the registry array
-
# will be tagged as <tt>!ruby/object:MIME::Type</tt>.
-
#
-
# Note that the YAML format is about 2½ times *slower* than the JSON format.
-
#
-
# NOTE: The purpose of this format is purely for maintenance reasons.
-
2
def load_yaml
-
Dir[yaml_path].sort.each do |f|
-
container.add(*self.class.load_from_yaml(f), :silent)
-
end
-
container
-
end
-
-
# Loads a MIME::Types registry from JSON files (<tt>*.json</tt>)
-
# recursively found in +path+.
-
#
-
# It is expected that the JSON objects will be an array of hash objects.
-
# The JSON format is the registry format for the MIME types registry
-
# shipped with the mime-types library.
-
2
def load_json
-
Dir[json_path].sort.each do |f|
-
types = self.class.load_from_json(f)
-
container.add(*types, :silent)
-
end
-
container
-
end
-
-
# Loads a MIME::Types registry from columnar files recursively found in
-
# +path+.
-
2
def load_columnar
-
2
require 'mime/types/columnar' unless defined?(MIME::Types::Columnar)
-
2
container.extend(MIME::Types::Columnar)
-
2
container.load_base_data(path)
-
-
2
container
-
end
-
-
# Loads a MIME::Types registry. Loads from JSON files by default
-
# (#load_json).
-
#
-
# This will load from columnar files (#load_columnar) if <tt>columnar:
-
# true</tt> is provided in +options+ and there are columnar files in +path+.
-
2
def load(options = { columnar: false })
-
2
if options[:columnar] && !Dir[columnar_path].empty?
-
2
load_columnar
-
else
-
load_json
-
end
-
end
-
-
2
class << self
-
# Loads the default MIME::Type registry.
-
2
def load(options = { columnar: false })
-
2
new.load(options)
-
end
-
-
# Loads MIME::Types from a single YAML file.
-
#
-
# It is expected that the YAML objects contained within the registry
-
# array will be tagged as <tt>!ruby/object:MIME::Type</tt>.
-
#
-
# Note that the YAML format is about 2½ times *slower* than the JSON
-
# format.
-
#
-
# NOTE: The purpose of this format is purely for maintenance reasons.
-
2
def load_from_yaml(filename)
-
begin
-
require 'psych'
-
rescue LoadError
-
nil
-
end
-
require 'yaml'
-
YAML.load(read_file(filename))
-
end
-
-
# Loads MIME::Types from a single JSON file.
-
#
-
# It is expected that the JSON objects will be an array of hash objects.
-
# The JSON format is the registry format for the MIME types registry
-
# shipped with the mime-types library.
-
2
def load_from_json(filename)
-
require 'json'
-
JSON.parse(read_file(filename)).map { |type| MIME::Type.new(type) }
-
end
-
-
2
private
-
-
2
def read_file(filename)
-
File.open(filename, 'r:UTF-8:-', &:read)
-
end
-
end
-
-
2
private
-
-
2
def yaml_path
-
File.join(path, '*.y{,a}ml')
-
end
-
-
2
def json_path
-
File.join(path, '*.json')
-
end
-
-
2
def columnar_path
-
2
File.join(path, '*.column')
-
end
-
end
-
# -*- ruby encoding: utf-8 -*-
-
-
2
require 'logger'
-
-
##
-
2
module MIME
-
##
-
2
class Types
-
2
class << self
-
# Configure the MIME::Types logger. This defaults to an instance of a
-
# logger that passes messages (unformatted) through to Kernel#warn.
-
2
attr_accessor :logger
-
end
-
-
2
class WarnLogger < ::Logger #:nodoc:
-
2
class WarnLogDevice < ::Logger::LogDevice #:nodoc:
-
2
def initialize(*)
-
end
-
-
2
def write(m)
-
Kernel.warn(m)
-
end
-
-
2
def close
-
end
-
end
-
-
2
def initialize(_1, _2 = nil, _3 = nil)
-
2
super nil
-
2
@logdev = WarnLogDevice.new
-
2
@formatter = ->(_s, _d, _p, m) { m }
-
end
-
end
-
-
2
self.logger = WarnLogger.new(nil)
-
end
-
end
-
2
class << MIME::Types
-
2
include Enumerable
-
-
##
-
2
def new(*) # :nodoc:
-
2
super.tap do |types|
-
2
__instances__.add types
-
end
-
end
-
-
# MIME::Types#[] against the default MIME::Types registry.
-
2
def [](type_id, complete: false, registered: false)
-
__types__[type_id, complete: complete, registered: registered]
-
end
-
-
# MIME::Types#count against the default MIME::Types registry.
-
2
def count
-
__types__.count
-
end
-
-
# MIME::Types#each against the default MIME::Types registry.
-
2
def each
-
if block_given?
-
__types__.each { |t| yield t }
-
else
-
enum_for(:each)
-
end
-
end
-
-
# MIME::Types#type_for against the default MIME::Types registry.
-
2
def type_for(filename)
-
__types__.type_for(filename)
-
end
-
2
alias_method :of, :type_for
-
-
# MIME::Types#add against the default MIME::Types registry.
-
2
def add(*types)
-
__types__.add(*types)
-
end
-
-
2
private
-
-
2
def lazy_load?
-
2
(lazy = ENV['RUBY_MIME_TYPES_LAZY_LOAD']) && (lazy != 'false')
-
end
-
-
2
def __types__
-
(defined?(@__types__) and @__types__) or load_default_mime_types
-
end
-
-
2
unless private_method_defined?(:load_mode)
-
2
def load_mode
-
2
{ columnar: true }
-
end
-
end
-
-
2
def load_default_mime_types(mode = load_mode)
-
2
@__types__ = MIME::Types::Cache.load
-
2
unless @__types__
-
2
@__types__ = MIME::Types::Loader.load(mode)
-
2
MIME::Types::Cache.save(@__types__)
-
end
-
2
@__types__
-
end
-
-
2
def __instances__
-
3930
@__instances__ ||= Set.new
-
end
-
-
2
def reindex_extensions(type)
-
3928
__instances__.each do |instance|
-
3928
instance.send(:reindex_extensions!, type)
-
end
-
3928
true
-
end
-
end
-
-
##
-
2
class MIME::Types
-
2
load_default_mime_types(load_mode) unless lazy_load?
-
end
-
# frozen_string_literal: true
-
-
2
module MIME
-
2
class Types
-
2
module Data
-
2
VERSION = '3.2016.0521'
-
-
# The path that will be used for loading the MIME::Types data. The
-
# default location is __FILE__/../../../../data, which is where the data
-
# lives in the gem installation of the mime-types-data library.
-
#
-
# The MIME::Types::Loader will load all JSON or columnar files contained
-
# in this path.
-
#
-
# System maintainer note: this is the constant to change when packaging
-
# mime-types for your system. It is recommended that the path be
-
# something like /usr/share/ruby/mime-types/.
-
2
PATH = File.expand_path('../../../../data', __FILE__)
-
end
-
end
-
end
-
2
require "optparse"
-
2
require "thread"
-
2
require "mutex_m"
-
2
require "minitest/parallel"
-
-
##
-
# :include: README.rdoc
-
-
2
module Minitest
-
2
VERSION = "5.9.0" # :nodoc:
-
2
ENCS = "".respond_to? :encoding # :nodoc:
-
-
2
@@installed_at_exit ||= false
-
2
@@after_run = []
-
2
@extensions = []
-
-
4
mc = (class << self; self; end)
-
-
##
-
# Parallel test executor
-
-
2
mc.send :attr_accessor, :parallel_executor
-
2
self.parallel_executor = Parallel::Executor.new((ENV["N"] || 2).to_i)
-
-
##
-
# Filter object for backtraces.
-
-
2
mc.send :attr_accessor, :backtrace_filter
-
-
##
-
# Reporter object to be used for all runs.
-
#
-
# NOTE: This accessor is only available during setup, not during runs.
-
-
2
mc.send :attr_accessor, :reporter
-
-
##
-
# Names of known extension plugins.
-
-
2
mc.send :attr_accessor, :extensions
-
-
##
-
# The signal to use for dumping information to STDERR. Defaults to "INFO".
-
-
2
mc.send :attr_accessor, :info_signal
-
2
self.info_signal = "INFO"
-
-
##
-
# Registers Minitest to run at process exit
-
-
2
def self.autorun
-
at_exit {
-
2
next if $! and not ($!.kind_of? SystemExit and $!.success?)
-
-
2
exit_code = nil
-
-
2
at_exit {
-
2
@@after_run.reverse_each(&:call)
-
2
exit exit_code || false
-
}
-
-
2
exit_code = Minitest.run ARGV
-
2
} unless @@installed_at_exit
-
2
@@installed_at_exit = true
-
end
-
-
##
-
# A simple hook allowing you to run a block of code after everything
-
# is done running. Eg:
-
#
-
# Minitest.after_run { p $debugging_info }
-
-
2
def self.after_run &block
-
@@after_run << block
-
end
-
-
2
def self.init_plugins options # :nodoc:
-
2
self.extensions.each do |name|
-
2
msg = "plugin_#{name}_init"
-
2
send msg, options if self.respond_to? msg
-
end
-
end
-
-
2
def self.load_plugins # :nodoc:
-
2
return unless self.extensions.empty?
-
-
2
seen = {}
-
-
2
require "rubygems" unless defined? Gem
-
-
2
Gem.find_files("minitest/*_plugin.rb").each do |plugin_path|
-
2
name = File.basename plugin_path, "_plugin.rb"
-
-
2
next if seen[name]
-
2
seen[name] = true
-
-
2
require plugin_path
-
2
self.extensions << name
-
end
-
end
-
-
##
-
# This is the top-level run method. Everything starts from here. It
-
# tells each Runnable sub-class to run, and each of those are
-
# responsible for doing whatever they do.
-
#
-
# The overall structure of a run looks like this:
-
#
-
# Minitest.autorun
-
# Minitest.run(args)
-
# Minitest.__run(reporter, options)
-
# Runnable.runnables.each
-
# runnable.run(reporter, options)
-
# self.runnable_methods.each
-
# self.run_one_method(self, runnable_method, reporter)
-
# Minitest.run_one_method(klass, runnable_method)
-
# klass.new(runnable_method).run
-
-
2
def self.run args = []
-
2
self.load_plugins
-
-
2
options = process_args args
-
-
2
reporter = CompositeReporter.new
-
2
reporter << SummaryReporter.new(options[:io], options)
-
2
reporter << ProgressReporter.new(options[:io], options)
-
-
2
self.reporter = reporter # this makes it available to plugins
-
2
self.init_plugins options
-
2
self.reporter = nil # runnables shouldn't depend on the reporter, ever
-
-
2
self.parallel_executor.start if parallel_executor.respond_to?(:start)
-
2
reporter.start
-
2
begin
-
2
__run reporter, options
-
rescue Interrupt
-
warn "Interrupted. Exiting..."
-
end
-
2
self.parallel_executor.shutdown
-
2
reporter.report
-
-
2
reporter.passed?
-
end
-
-
##
-
# Internal run method. Responsible for telling all Runnable
-
# sub-classes to run.
-
-
2
def self.__run reporter, options
-
2
suites = Runnable.runnables.shuffle
-
24
parallel, serial = suites.partition { |s| s.test_order == :parallel }
-
-
# If we run the parallel tests before the serial tests, the parallel tests
-
# could run in parallel with the serial tests. This would be bad because
-
# the serial tests won't lock around Reporter#record. Run the serial tests
-
# first, so that after they complete, the parallel tests will lock when
-
# recording results.
-
22
serial.map { |suite| suite.run reporter, options } +
-
2
parallel.map { |suite| suite.run reporter, options }
-
end
-
-
2
def self.process_args args = [] # :nodoc:
-
2
options = {
-
:io => $stdout,
-
}
-
2
orig_args = args.dup
-
-
2
OptionParser.new do |opts|
-
2
opts.banner = "minitest options:"
-
2
opts.version = Minitest::VERSION
-
-
2
opts.on "-h", "--help", "Display this help." do
-
puts opts
-
exit
-
end
-
-
2
desc = "Sets random seed. Also via env. Eg: SEED=n rake"
-
2
opts.on "-s", "--seed SEED", Integer, desc do |m|
-
options[:seed] = m.to_i
-
end
-
-
2
opts.on "-v", "--verbose", "Verbose. Show progress processing files." do
-
options[:verbose] = true
-
end
-
-
2
opts.on "-n", "--name PATTERN", "Filter run on /regexp/ or string." do |a|
-
options[:filter] = a
-
end
-
-
2
opts.on "-e", "--exclude PATTERN", "Exclude /regexp/ or string from run." do |a|
-
options[:exclude] = a
-
end
-
-
2
unless extensions.empty?
-
2
opts.separator ""
-
2
opts.separator "Known extensions: #{extensions.join(", ")}"
-
-
2
extensions.each do |meth|
-
2
msg = "plugin_#{meth}_options"
-
2
send msg, opts, options if self.respond_to?(msg)
-
end
-
end
-
-
2
begin
-
2
opts.parse! args
-
rescue OptionParser::InvalidOption => e
-
puts
-
puts e
-
puts
-
puts opts
-
exit 1
-
end
-
-
2
orig_args -= args
-
end
-
-
2
unless options[:seed] then
-
2
srand
-
2
options[:seed] = (ENV["SEED"] || srand).to_i % 0xFFFF
-
2
orig_args << "--seed" << options[:seed].to_s
-
end
-
-
2
srand options[:seed]
-
-
2
options[:args] = orig_args.map { |s|
-
4
s =~ /[\s|&<>$()]/ ? s.inspect : s
-
}.join " "
-
-
2
options
-
end
-
-
2
def self.filter_backtrace bt # :nodoc:
-
4
backtrace_filter.filter bt
-
end
-
-
##
-
# Represents anything "runnable", like Test, Spec, Benchmark, or
-
# whatever you can dream up.
-
#
-
# Subclasses of this are automatically registered and available in
-
# Runnable.runnables.
-
-
2
class Runnable
-
##
-
# Number of assertions executed in this run.
-
-
2
attr_accessor :assertions
-
-
##
-
# An assertion raised during the run, if any.
-
-
2
attr_accessor :failures
-
-
##
-
# Name of the run.
-
-
2
def name
-
161
@NAME
-
end
-
-
##
-
# Set the name of the run.
-
-
2
def name= o
-
39
@NAME = o
-
end
-
-
2
def self.inherited klass # :nodoc:
-
22
self.runnables << klass
-
22
super
-
end
-
-
##
-
# Returns all instance methods matching the pattern +re+.
-
-
2
def self.methods_matching re
-
22
public_instance_methods(true).grep(re).map(&:to_s)
-
end
-
-
2
def self.reset # :nodoc:
-
2
@@runnables = []
-
end
-
-
2
reset
-
-
##
-
# Responsible for running all runnable methods in a given class,
-
# each in its own instance. Each instance is passed to the
-
# reporter to record.
-
-
2
def self.run reporter, options = {}
-
22
filter = options[:filter] || "/./"
-
22
filter = Regexp.new $1 if filter =~ %r%/(.*)/%
-
-
22
filtered_methods = self.runnable_methods.find_all { |m|
-
39
filter === m || filter === "#{self}##{m}"
-
}
-
-
22
exclude = options[:exclude]
-
22
exclude = Regexp.new $1 if exclude =~ %r%/(.*)/%
-
-
22
filtered_methods.delete_if { |m|
-
39
exclude === m || exclude === "#{self}##{m}"
-
}
-
-
22
return if filtered_methods.empty?
-
-
8
with_info_handler reporter do
-
8
filtered_methods.each do |method_name|
-
39
run_one_method self, method_name, reporter
-
end
-
end
-
end
-
-
##
-
# Runs a single method and has the reporter record the result.
-
# This was considered internal API but is factored out of run so
-
# that subclasses can specialize the running of an individual
-
# test. See Minitest::ParallelTest::ClassMethods for an example.
-
-
2
def self.run_one_method klass, method_name, reporter
-
39
reporter.record Minitest.run_one_method(klass, method_name)
-
end
-
-
2
def self.with_info_handler reporter, &block # :nodoc:
-
8
handler = lambda do
-
unless reporter.passed? then
-
warn "Current results:"
-
warn ""
-
warn reporter.reporters.first
-
warn ""
-
end
-
end
-
-
8
on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
2
SIGNALS = Signal.list # :nodoc:
-
-
2
def self.on_signal name, action # :nodoc:
-
47
supported = SIGNALS[name]
-
-
old_trap = trap name do
-
old_trap.call if old_trap.respond_to? :call
-
action.call
-
47
end if supported
-
-
47
yield
-
ensure
-
47
trap name, old_trap if supported
-
end
-
-
##
-
# Each subclass of Runnable is responsible for overriding this
-
# method to return all runnable methods. See #methods_matching.
-
-
2
def self.runnable_methods
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns all subclasses of Runnable.
-
-
2
def self.runnables
-
24
@@runnables
-
end
-
-
2
def marshal_dump # :nodoc:
-
[self.name, self.failures, self.assertions]
-
end
-
-
2
def marshal_load ary # :nodoc:
-
self.name, self.failures, self.assertions = ary
-
end
-
-
2
def failure # :nodoc:
-
173
self.failures.first
-
end
-
-
2
def initialize name # :nodoc:
-
39
self.name = name
-
39
self.failures = []
-
39
self.assertions = 0
-
end
-
-
##
-
# Runs a single method. Needs to return self.
-
-
2
def run
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
2
def passed?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns a single character string to print based on the result
-
# of the run. Eg ".", "F", or "E".
-
-
2
def result_code
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Was this run skipped? See #passed? for more information.
-
-
2
def skipped?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
end
-
-
##
-
# Defines the API for Reporters. Subclass this and override whatever
-
# you want. Go nuts.
-
-
2
class AbstractReporter
-
2
include Mutex_m
-
-
##
-
# Starts reporting on the run.
-
-
2
def start
-
end
-
-
##
-
# Record a result and output the Runnable#result_code. Stores the
-
# result of the run if the run did not pass.
-
-
2
def record result
-
end
-
-
##
-
# Outputs the summary of the run.
-
-
2
def report
-
end
-
-
##
-
# Did this run pass?
-
-
2
def passed?
-
true
-
end
-
end
-
-
2
class Reporter < AbstractReporter # :nodoc:
-
##
-
# The IO used to report.
-
-
2
attr_accessor :io
-
-
##
-
# Command-line options for this run.
-
-
2
attr_accessor :options
-
-
2
def initialize io = $stdout, options = {} # :nodoc:
-
4
super()
-
4
self.io = io
-
4
self.options = options
-
end
-
end
-
-
##
-
# A very simple reporter that prints the "dots" during the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
2
class ProgressReporter < Reporter
-
2
def record result # :nodoc:
-
io.print "%s#%s = %.2f s = " % [result.class, result.name, result.time] if
-
39
options[:verbose]
-
39
io.print result.result_code
-
39
io.puts if options[:verbose]
-
end
-
end
-
-
##
-
# A reporter that gathers statistics about a test run. Does not do
-
# any IO because meant to be used as a parent class for a reporter
-
# that does.
-
#
-
# If you want to create an entirely different type of output (eg,
-
# CI, HTML, etc), this is the place to start.
-
-
2
class StatisticsReporter < Reporter
-
# :stopdoc:
-
2
attr_accessor :assertions
-
2
attr_accessor :count
-
2
attr_accessor :results
-
2
attr_accessor :start_time
-
2
attr_accessor :total_time
-
2
attr_accessor :failures
-
2
attr_accessor :errors
-
2
attr_accessor :skips
-
# :startdoc:
-
-
2
def initialize io = $stdout, options = {} # :nodoc:
-
2
super
-
-
2
self.assertions = 0
-
2
self.count = 0
-
2
self.results = []
-
2
self.start_time = nil
-
2
self.total_time = nil
-
2
self.failures = nil
-
2
self.errors = nil
-
2
self.skips = nil
-
end
-
-
2
def passed? # :nodoc:
-
2
results.all?(&:skipped?)
-
end
-
-
2
def start # :nodoc:
-
2
self.start_time = Minitest.clock_time
-
end
-
-
2
def record result # :nodoc:
-
39
self.count += 1
-
39
self.assertions += result.assertions
-
-
39
results << result if not result.passed? or result.skipped?
-
end
-
-
2
def report # :nodoc:
-
9
aggregate = results.group_by { |r| r.failure.class }
-
2
aggregate.default = [] # dumb. group_by should provide this
-
-
2
self.total_time = Minitest.clock_time - start_time
-
2
self.failures = aggregate[Assertion].size
-
2
self.errors = aggregate[UnexpectedError].size
-
2
self.skips = aggregate[Skip].size
-
end
-
end
-
-
##
-
# A reporter that prints the header, summary, and failure details at
-
# the end of the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
2
class SummaryReporter < StatisticsReporter
-
# :stopdoc:
-
2
attr_accessor :sync
-
2
attr_accessor :old_sync
-
# :startdoc:
-
-
2
def start # :nodoc:
-
2
super
-
-
2
io.puts "Run options: #{options[:args]}"
-
2
io.puts
-
2
io.puts "# Running:"
-
2
io.puts
-
-
2
self.sync = io.respond_to? :"sync=" # stupid emacs
-
2
self.old_sync, io.sync = io.sync, true if self.sync
-
end
-
-
2
def report # :nodoc:
-
2
super
-
-
2
io.sync = self.old_sync
-
-
2
io.puts unless options[:verbose] # finish the dots
-
2
io.puts
-
2
io.puts statistics
-
2
io.puts aggregated_results
-
2
io.puts summary
-
end
-
-
2
def statistics # :nodoc:
-
"Finished in %.6fs, %.4f runs/s, %.4f assertions/s." %
-
2
[total_time, count / total_time, assertions / total_time]
-
end
-
-
2
def aggregated_results # :nodoc:
-
2
filtered_results = results.dup
-
2
filtered_results.reject!(&:skipped?) unless options[:verbose]
-
-
2
s = filtered_results.each_with_index.map { |result, i|
-
7
"\n%3d) %s" % [i+1, result]
-
}.join("\n") + "\n"
-
-
s.force_encoding(io.external_encoding) if
-
2
ENCS and io.external_encoding and s.encoding != io.external_encoding
-
-
2
s
-
end
-
-
2
alias to_s aggregated_results
-
-
2
def summary # :nodoc:
-
2
extra = ""
-
-
extra = "\n\nYou have skipped tests. Run with --verbose for details." if
-
2
results.any?(&:skipped?) unless options[:verbose] or ENV["MT_NO_SKIP_MSG"]
-
-
"%d runs, %d assertions, %d failures, %d errors, %d skips%s" %
-
2
[count, assertions, failures, errors, skips, extra]
-
end
-
end
-
-
##
-
# Dispatch to multiple reporters as one.
-
-
2
class CompositeReporter < AbstractReporter
-
##
-
# The list of reporters to dispatch to.
-
-
2
attr_accessor :reporters
-
-
2
def initialize *reporters # :nodoc:
-
2
super()
-
2
self.reporters = reporters
-
end
-
-
2
def io # :nodoc:
-
reporters.first.io
-
end
-
-
##
-
# Add another reporter to the mix.
-
-
2
def << reporter
-
4
self.reporters << reporter
-
end
-
-
2
def passed? # :nodoc:
-
2
self.reporters.all?(&:passed?)
-
end
-
-
2
def start # :nodoc:
-
2
self.reporters.each(&:start)
-
end
-
-
2
def record result # :nodoc:
-
39
self.reporters.each do |reporter|
-
78
reporter.record result
-
end
-
end
-
-
2
def report # :nodoc:
-
2
self.reporters.each(&:report)
-
end
-
end
-
-
##
-
# Represents run failures.
-
-
2
class Assertion < Exception
-
2
def error # :nodoc:
-
self
-
end
-
-
##
-
# Where was this run before an assertion was raised?
-
-
2
def location
-
3
last_before_assertion = ""
-
3
self.backtrace.reverse_each do |s|
-
69
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
-
66
last_before_assertion = s
-
end
-
3
last_before_assertion.sub(/:in .*$/, "")
-
end
-
-
2
def result_code # :nodoc:
-
7
result_label[0, 1]
-
end
-
-
2
def result_label # :nodoc:
-
6
"Failure"
-
end
-
end
-
-
##
-
# Assertion raised when skipping a run.
-
-
2
class Skip < Assertion
-
2
def result_label # :nodoc:
-
"Skipped"
-
end
-
end
-
-
##
-
# Assertion wrapping an unexpected error that was raised during a run.
-
-
2
class UnexpectedError < Assertion
-
2
attr_accessor :exception # :nodoc:
-
-
2
def initialize exception # :nodoc:
-
4
super "Unexpected exception"
-
4
self.exception = exception
-
end
-
-
2
def backtrace # :nodoc:
-
4
self.exception.backtrace
-
end
-
-
2
def error # :nodoc:
-
self.exception
-
end
-
-
2
def message # :nodoc:
-
4
bt = Minitest.filter_backtrace(self.backtrace).join "\n "
-
4
"#{self.exception.class}: #{self.exception.message}\n #{bt}"
-
end
-
-
2
def result_label # :nodoc:
-
8
"Error"
-
end
-
end
-
-
##
-
# Provides a simple set of guards that you can use in your tests
-
# to skip execution if it is not applicable. These methods are
-
# mixed into Test as both instance and class methods so you
-
# can use them inside or outside of the test methods.
-
#
-
# def test_something_for_mri
-
# skip "bug 1234" if jruby?
-
# # ...
-
# end
-
#
-
# if windows? then
-
# # ... lots of test methods ...
-
# end
-
-
2
module Guard
-
-
##
-
# Is this running on jruby?
-
-
2
def jruby? platform = RUBY_PLATFORM
-
"java" == platform
-
end
-
-
##
-
# Is this running on maglev?
-
-
2
def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
"maglev" == platform
-
end
-
-
##
-
# Is this running on mri?
-
-
2
def mri? platform = RUBY_DESCRIPTION
-
/^ruby/ =~ platform
-
end
-
-
##
-
# Is this running on rubinius?
-
-
2
def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
"rbx" == platform
-
end
-
-
##
-
# Is this running on windows?
-
-
2
def windows? platform = RUBY_PLATFORM
-
/mswin|mingw/ =~ platform
-
end
-
end
-
-
2
class BacktraceFilter # :nodoc:
-
2
def filter bt
-
return ["No backtrace"] unless bt
-
-
return bt.dup if $DEBUG
-
-
new_bt = bt.take_while { |line| line !~ /lib\/minitest/ }
-
new_bt = bt.select { |line| line !~ /lib\/minitest/ } if new_bt.empty?
-
new_bt = bt.dup if new_bt.empty?
-
-
new_bt
-
end
-
end
-
-
2
self.backtrace_filter = BacktraceFilter.new
-
-
2
def self.run_one_method klass, method_name # :nodoc:
-
39
result = klass.new(method_name).run
-
39
raise "#{klass}#run _must_ return self" unless klass === result
-
39
result
-
end
-
-
2
if defined? Process::CLOCK_MONOTONIC
-
2
def self.clock_time
-
121
Process.clock_gettime Process::CLOCK_MONOTONIC
-
end
-
else
-
def self.clock_time
-
Time.now
-
end
-
end
-
end
-
-
2
require "minitest/test"
-
# encoding: UTF-8
-
-
2
require "rbconfig"
-
2
require "tempfile"
-
2
require "stringio"
-
-
2
module Minitest
-
##
-
# Minitest Assertions. All assertion methods accept a +msg+ which is
-
# printed if the assertion fails.
-
#
-
# Protocol: Nearly everything here boils up to +assert+, which
-
# expects to be able to increment an instance accessor named
-
# +assertions+. This is not provided by Assertions and must be
-
# provided by the thing including Assertions. See Minitest::Runnable
-
# for an example.
-
-
2
module Assertions
-
2
UNDEFINED = Object.new # :nodoc:
-
-
2
def UNDEFINED.inspect # :nodoc:
-
"UNDEFINED" # again with the rdoc bugs... :(
-
end
-
-
##
-
# Returns the diff command to use in #diff. Tries to intelligently
-
# figure out what diff to use.
-
-
2
def self.diff
-
@diff = if (RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ &&
-
system("diff.exe", __FILE__, __FILE__)) then
-
"diff.exe -u"
-
elsif Minitest::Test.maglev? then
-
"diff -u"
-
elsif system("gdiff", __FILE__, __FILE__)
-
"gdiff -u" # solaris and kin suck
-
elsif system("diff", __FILE__, __FILE__)
-
"diff -u"
-
else
-
nil
-
end unless defined? @diff
-
-
@diff
-
end
-
-
##
-
# Set the diff command to use in #diff.
-
-
2
def self.diff= o
-
@diff = o
-
end
-
-
##
-
# Returns a diff between +exp+ and +act+. If there is no known
-
# diff command or if it doesn't make sense to diff the output
-
# (single line, short output), then it simply returns a basic
-
# comparison between the two.
-
-
2
def diff exp, act
-
expect = mu_pp_for_diff exp
-
butwas = mu_pp_for_diff act
-
result = nil
-
-
need_to_diff =
-
(expect.include?("\n") ||
-
butwas.include?("\n") ||
-
expect.size > 30 ||
-
butwas.size > 30 ||
-
expect == butwas) &&
-
Minitest::Assertions.diff
-
-
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
-
need_to_diff
-
-
Tempfile.open("expect") do |a|
-
a.puts expect
-
a.flush
-
-
Tempfile.open("butwas") do |b|
-
b.puts butwas
-
b.flush
-
-
result = `#{Minitest::Assertions.diff} #{a.path} #{b.path}`
-
result.sub!(/^\-\-\- .+/, "--- expected")
-
result.sub!(/^\+\+\+ .+/, "+++ actual")
-
-
if result.empty? then
-
klass = exp.class
-
result = [
-
"No visible difference in the #{klass}#inspect output.\n",
-
"You should look at the implementation of #== on ",
-
"#{klass} or its members.\n",
-
expect,
-
].join
-
end
-
end
-
end
-
-
result
-
end
-
-
##
-
# This returns a human-readable version of +obj+. By default
-
# #inspect is called. You can override this to use #pretty_print
-
# if you want.
-
-
2
def mu_pp obj
-
s = obj.inspect
-
-
if defined? Encoding then
-
s = s.encode Encoding.default_external
-
-
if String === obj && obj.encoding != Encoding.default_external then
-
s = "# encoding: #{obj.encoding}\n#{s}"
-
end
-
end
-
-
s
-
end
-
-
##
-
# This returns a diff-able human-readable version of +obj+. This
-
# differs from the regular mu_pp because it expands escaped
-
# newlines and makes hex-values generic (like object_ids). This
-
# uses mu_pp to do the first pass and then cleans it up.
-
-
2
def mu_pp_for_diff obj
-
mu_pp(obj).gsub(/\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ":0xXXXXXX")
-
end
-
-
##
-
# Fails unless +test+ is truthy.
-
-
2
def assert test, msg = nil
-
56
self.assertions += 1
-
56
unless test then
-
3
msg ||= "Expected #{mu_pp test} to be truthy."
-
3
msg = msg.call if Proc === msg
-
3
raise Minitest::Assertion, msg
-
end
-
53
true
-
end
-
-
2
def _synchronize # :nodoc:
-
yield
-
end
-
-
##
-
# Fails unless +obj+ is empty.
-
-
2
def assert_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
-
assert_respond_to obj, :empty?
-
assert obj.empty?, msg
-
end
-
-
2
E = "" # :nodoc:
-
-
##
-
# Fails unless <tt>exp == act</tt> printing the difference between
-
# the two, if possible.
-
#
-
# If there is no visible difference but the assertion fails, you
-
# should suspect that your #== is buggy, or your inspect output is
-
# missing crucial details. For nicer structural diffing, set
-
# Minitest::Test.make_my_diffs_pretty!
-
#
-
# For floats use assert_in_delta.
-
#
-
# See also: Minitest::Assertions.diff
-
-
2
def assert_equal exp, act, msg = nil
-
11
msg = message(msg, E) { diff exp, act }
-
11
assert exp == act, msg
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
-
# of each other.
-
#
-
# assert_in_delta Math::PI, (22.0 / 7.0), 0.01
-
-
2
def assert_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
-
}
-
assert delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ have a relative
-
# error less than +epsilon+.
-
-
2
def assert_in_epsilon a, b, epsilon = 0.001, msg = nil
-
assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg
-
end
-
-
##
-
# Fails unless +collection+ includes +obj+.
-
-
2
def assert_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
assert collection.include?(obj), msg
-
end
-
-
##
-
# Fails unless +obj+ is an instance of +cls+.
-
-
2
def assert_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
-
}
-
-
assert obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails unless +obj+ is a kind of +cls+.
-
-
2
def assert_kind_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
-
-
assert obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails unless +matcher+ <tt>=~</tt> +obj+.
-
-
2
def assert_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
assert matcher =~ obj, msg
-
end
-
-
##
-
# Fails unless +obj+ is nil
-
-
2
def assert_nil obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
-
assert obj.nil?, msg
-
end
-
-
##
-
# For testing with binary operators. Eg:
-
#
-
# assert_operator 5, :<=, 4
-
-
2
def assert_operator o1, op, o2 = UNDEFINED, msg = nil
-
9
return assert_predicate o1, op, msg if UNDEFINED == o2
-
9
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
-
9
assert o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if stdout or stderr do not output the expected results.
-
# Pass in nil if you don't care about that streams output. Pass in
-
# "" if you require it to be silent. Pass in a regexp if you want
-
# to pattern match.
-
#
-
# assert_output(/hey/) { method_with_output }
-
#
-
# NOTE: this uses #capture_io, not #capture_subprocess_io.
-
#
-
# See also: #assert_silent
-
-
2
def assert_output stdout = nil, stderr = nil
-
out, err = capture_io do
-
yield
-
end
-
-
err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
-
out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
-
-
y = send err_msg, stderr, err, "In stderr" if err_msg
-
x = send out_msg, stdout, out, "In stdout" if out_msg
-
-
(!stdout || x) && (!stderr || y)
-
end
-
-
##
-
# For testing with predicates. Eg:
-
#
-
# assert_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by assert_operator:
-
#
-
# str.must_be :empty?
-
-
2
def assert_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
-
assert o1.__send__(op), msg
-
end
-
-
##
-
# Fails unless the block raises one of +exp+. Returns the
-
# exception matched so you can check the message, attributes, etc.
-
#
-
# +exp+ takes an optional message on the end to help explain
-
# failures and defaults to StandardError if no exception class is
-
# passed.
-
-
2
def assert_raises *exp
-
msg = "#{exp.pop}.\n" if String === exp.last
-
exp << StandardError if exp.empty?
-
-
begin
-
yield
-
rescue *exp => e
-
pass # count assertion
-
return e
-
rescue Minitest::Skip, Minitest::Assertion
-
# don't count assertion
-
raise
-
rescue SignalException, SystemExit
-
raise
-
rescue Exception => e
-
flunk proc {
-
exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
-
}
-
end
-
-
exp = exp.first if exp.size == 1
-
-
flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
-
end
-
-
##
-
# Fails unless +obj+ responds to +meth+.
-
-
2
def assert_respond_to obj, meth, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
-
}
-
assert obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails unless +exp+ and +act+ are #equal?
-
-
2
def assert_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
-
}
-
assert exp.equal?(act), msg
-
end
-
-
##
-
# +send_ary+ is a receiver, message and arguments.
-
#
-
# Fails unless the call returns a true value
-
-
2
def assert_send send_ary, m = nil
-
recv, msg, *args = send_ary
-
m = message(m) {
-
"Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
-
assert recv.__send__(msg, *args), m
-
end
-
-
##
-
# Fails if the block outputs anything to stderr or stdout.
-
#
-
# See also: #assert_output
-
-
2
def assert_silent
-
assert_output "", "" do
-
yield
-
end
-
end
-
-
##
-
# Fails unless the block throws +sym+
-
-
2
def assert_throws sym, msg = nil
-
default = "Expected #{mu_pp(sym)} to have been thrown"
-
caught = true
-
catch(sym) do
-
begin
-
yield
-
rescue ThreadError => e # wtf?!? 1.8 + threads == suck
-
default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
-
rescue ArgumentError => e # 1.9 exception
-
default += ", not #{e.message.split(/ /).last}"
-
rescue NameError => e # 1.8 exception
-
default += ", not #{e.name.inspect}"
-
end
-
caught = false
-
end
-
-
assert caught, message(msg) { default }
-
end
-
-
##
-
# Captures $stdout and $stderr into strings:
-
#
-
# out, err = capture_io do
-
# puts "Some info"
-
# warn "You did a bad thing"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: For efficiency, this method uses StringIO and does not
-
# capture IO for subprocesses. Use #capture_subprocess_io for
-
# that.
-
-
2
def capture_io
-
_synchronize do
-
begin
-
captured_stdout, captured_stderr = StringIO.new, StringIO.new
-
-
orig_stdout, orig_stderr = $stdout, $stderr
-
$stdout, $stderr = captured_stdout, captured_stderr
-
-
yield
-
-
return captured_stdout.string, captured_stderr.string
-
ensure
-
$stdout = orig_stdout
-
$stderr = orig_stderr
-
end
-
end
-
end
-
-
##
-
# Captures $stdout and $stderr into strings, using Tempfile to
-
# ensure that subprocess IO is captured as well.
-
#
-
# out, err = capture_subprocess_io do
-
# system "echo Some info"
-
# system "echo You did a bad thing 1>&2"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: This method is approximately 10x slower than #capture_io so
-
# only use it when you need to test the output of a subprocess.
-
-
2
def capture_subprocess_io
-
_synchronize do
-
begin
-
require "tempfile"
-
-
captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err")
-
-
orig_stdout, orig_stderr = $stdout.dup, $stderr.dup
-
$stdout.reopen captured_stdout
-
$stderr.reopen captured_stderr
-
-
yield
-
-
$stdout.rewind
-
$stderr.rewind
-
-
return captured_stdout.read, captured_stderr.read
-
ensure
-
captured_stdout.unlink
-
captured_stderr.unlink
-
$stdout.reopen orig_stdout
-
$stderr.reopen orig_stderr
-
end
-
end
-
end
-
-
##
-
# Returns details for exception +e+
-
-
2
def exception_details e, msg
-
[
-
"#{msg}",
-
"Class: <#{e.class}>",
-
"Message: <#{e.message.inspect}>",
-
"---Backtrace---",
-
"#{Minitest.filter_backtrace(e.backtrace).join("\n")}",
-
"---------------",
-
].join "\n"
-
end
-
-
##
-
# Fails with +msg+
-
-
2
def flunk msg = nil
-
msg ||= "Epic Fail!"
-
assert false, msg
-
end
-
-
##
-
# Returns a proc that will output +msg+ along with the default message.
-
-
2
def message msg = nil, ending = nil, &default
-
22
proc {
-
msg = msg.call.chomp(".") if Proc === msg
-
custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
-
"#{custom_message}#{default.call}#{ending || "."}"
-
}
-
end
-
-
##
-
# used for counting assertions
-
-
2
def pass _msg = nil
-
assert true
-
end
-
-
##
-
# Fails if +test+ is truthy.
-
-
2
def refute test, msg = nil
-
2
msg ||= message { "Expected #{mu_pp(test)} to not be truthy" }
-
2
not assert !test, msg
-
end
-
-
##
-
# Fails if +obj+ is empty.
-
-
2
def refute_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
-
assert_respond_to obj, :empty?
-
refute obj.empty?, msg
-
end
-
-
##
-
# Fails if <tt>exp == act</tt>.
-
#
-
# For floats use refute_in_delta.
-
-
2
def refute_equal exp, act, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
-
}
-
refute exp == act, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
-
#
-
# refute_in_delta Math::PI, (22.0 / 7.0)
-
-
2
def refute_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
-
}
-
refute delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
-
# less than +epsilon+.
-
-
2
def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
-
refute_in_delta a, b, a * epsilon, msg
-
end
-
-
##
-
# Fails if +collection+ includes +obj+.
-
-
2
def refute_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
refute collection.include?(obj), msg
-
end
-
-
##
-
# Fails if +obj+ is an instance of +cls+.
-
-
2
def refute_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to not be an instance of #{cls}"
-
}
-
refute obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails if +obj+ is a kind of +cls+.
-
-
2
def refute_kind_of cls, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
-
refute obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails if +matcher+ <tt>=~</tt> +obj+.
-
-
2
def refute_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
refute matcher =~ obj, msg
-
end
-
-
##
-
# Fails if +obj+ is nil.
-
-
2
def refute_nil obj, msg = nil
-
2
msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
-
2
refute obj.nil?, msg
-
end
-
-
##
-
# Fails if +o1+ is not +op+ +o2+. Eg:
-
#
-
# refute_operator 1, :>, 2 #=> pass
-
# refute_operator 1, :<, 2 #=> fail
-
-
2
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
-
return refute_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}" }
-
refute o1.__send__(op, o2), msg
-
end
-
-
##
-
# For testing with predicates.
-
#
-
# refute_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by refute_operator:
-
#
-
# str.wont_be :empty?
-
-
2
def refute_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
-
refute o1.__send__(op), msg
-
end
-
-
##
-
# Fails if +obj+ responds to the message +meth+.
-
-
2
def refute_respond_to obj, meth, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
-
-
refute obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails if +exp+ is the same (by object identity) as +act+.
-
-
2
def refute_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
-
}
-
refute exp.equal?(act), msg
-
end
-
-
##
-
# Skips the current run. If run in verbose-mode, the skipped run
-
# gets listed at the end of the run but doesn't cause a failure
-
# exit code.
-
-
2
def skip msg = nil, bt = caller
-
msg ||= "Skipped, no message given"
-
@skip = true
-
raise Minitest::Skip, msg, bt
-
end
-
-
##
-
# Was this testcase skipped? Meant for #teardown.
-
-
2
def skipped?
-
defined?(@skip) and @skip
-
end
-
end
-
end
-
2
module Minitest
-
2
module Parallel
-
-
##
-
# The engine used to run multiple tests in parallel.
-
-
2
class Executor
-
-
##
-
# The size of the pool of workers.
-
-
2
attr_reader :size
-
-
##
-
# Create a parallel test executor of with +size+ workers.
-
-
2
def initialize size
-
2
@size = size
-
2
@queue = Queue.new
-
2
@pool = nil
-
end
-
-
##
-
# Start the executor
-
-
2
def start
-
2
@pool = size.times.map {
-
4
Thread.new(@queue) do |queue|
-
4
Thread.current.abort_on_exception = true
-
8
while (job = queue.pop)
-
klass, method, reporter = job
-
result = Minitest.run_one_method klass, method
-
reporter.synchronize { reporter.record result }
-
end
-
end
-
}
-
end
-
-
##
-
# Add a job to the queue
-
-
2
def << work; @queue << work; end
-
-
##
-
# Shuts down the pool of workers by signalling them to quit and
-
# waiting for them all to finish what they're currently working
-
# on.
-
-
2
def shutdown
-
6
size.times { @queue << nil }
-
2
@pool.each(&:join)
-
end
-
end
-
-
2
module Test
-
2
def _synchronize; Minitest::Test.io_lock.synchronize { yield }; end # :nodoc:
-
-
2
module ClassMethods # :nodoc:
-
2
def run_one_method klass, method_name, reporter
-
Minitest.parallel_executor << [klass, method_name, reporter]
-
end
-
-
2
def test_order
-
:parallel
-
end
-
end
-
end
-
end
-
end
-
2
require "minitest"
-
-
2
module Minitest
-
2
def self.plugin_pride_options opts, _options # :nodoc:
-
2
opts.on "-p", "--pride", "Pride. Show your testing pride!" do
-
PrideIO.pride!
-
end
-
end
-
-
2
def self.plugin_pride_init options # :nodoc:
-
2
if PrideIO.pride? then
-
klass = ENV["TERM"] =~ /^xterm|-256color$/ ? PrideLOL : PrideIO
-
io = klass.new options[:io]
-
-
self.reporter.reporters.grep(Minitest::Reporter).each do |rep|
-
rep.io = io if rep.io.tty?
-
end
-
end
-
end
-
-
##
-
# Show your testing pride!
-
-
2
class PrideIO
-
##
-
# Activate the pride plugin. Called from both -p option and minitest/pride
-
-
2
def self.pride!
-
@pride = true
-
end
-
-
##
-
# Are we showing our testing pride?
-
-
2
def self.pride?
-
2
@pride ||= false
-
end
-
-
# Start an escape sequence
-
2
ESC = "\e["
-
-
# End the escape sequence
-
2
NND = "#{ESC}0m"
-
-
# The IO we're going to pipe through.
-
2
attr_reader :io
-
-
2
def initialize io # :nodoc:
-
@io = io
-
# stolen from /System/Library/Perl/5.10.0/Term/ANSIColor.pm
-
# also reference http://en.wikipedia.org/wiki/ANSI_escape_code
-
@colors ||= (31..36).to_a
-
@size = @colors.size
-
@index = 0
-
end
-
-
##
-
# Wrap print to colorize the output.
-
-
2
def print o
-
case o
-
when "." then
-
io.print pride o
-
when "E", "F" then
-
io.print "#{ESC}41m#{ESC}37m#{o}#{NND}"
-
when "S" then
-
io.print pride o
-
else
-
io.print o
-
end
-
end
-
-
2
def puts(*o) # :nodoc:
-
o.map! { |s|
-
s.to_s.sub(/Finished/) {
-
@index = 0
-
"Fabulous run".split(//).map { |c|
-
pride(c)
-
}.join
-
}
-
}
-
-
io.puts(*o)
-
end
-
-
##
-
# Color a string.
-
-
2
def pride string
-
string = "*" if string == "."
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}#{c}m#{string}#{NND}"
-
end
-
-
2
def method_missing msg, *args # :nodoc:
-
io.send(msg, *args)
-
end
-
end
-
-
##
-
# If you thought the PrideIO was colorful...
-
#
-
# (Inspired by lolcat, but with clean math)
-
-
2
class PrideLOL < PrideIO
-
2
PI_3 = Math::PI / 3 # :nodoc:
-
-
2
def initialize io # :nodoc:
-
# walk red, green, and blue around a circle separated by equal thirds.
-
#
-
# To visualize, type this into wolfram-alpha:
-
#
-
# plot (3*sin(x)+3), (3*sin(x+2*pi/3)+3), (3*sin(x+4*pi/3)+3)
-
-
# 6 has wide pretty gradients. 3 == lolcat, about half the width
-
@colors = (0...(6 * 7)).map { |n|
-
n *= 1.0 / 6
-
r = (3 * Math.sin(n ) + 3).to_i
-
g = (3 * Math.sin(n + 2 * PI_3) + 3).to_i
-
b = (3 * Math.sin(n + 4 * PI_3) + 3).to_i
-
-
# Then we take rgb and encode them in a single number using base 6.
-
# For some mysterious reason, we add 16... to clear the bottom 4 bits?
-
# Yes... they're ugly.
-
-
36 * r + 6 * g + b + 16
-
}
-
-
super
-
end
-
-
##
-
# Make the string even more colorful. Damnit.
-
-
2
def pride string
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}38;5;#{c}m#{string}#{NND}"
-
end
-
end
-
end
-
2
require "minitest" unless defined? Minitest::Runnable
-
-
2
module Minitest
-
##
-
# Subclass Test to create your own tests. Typically you'll want a
-
# Test subclass per implementation class.
-
#
-
# See Minitest::Assertions
-
-
2
class Test < Runnable
-
2
require "minitest/assertions"
-
2
include Minitest::Assertions
-
-
2
PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, SystemExit] # :nodoc:
-
-
4
class << self; attr_accessor :io_lock; end # :nodoc:
-
2
self.io_lock = Mutex.new
-
-
##
-
# Call this at the top of your tests when you absolutely
-
# positively need to have ordered tests. In doing so, you're
-
# admitting that you suck and your tests are weak.
-
-
2
def self.i_suck_and_my_tests_are_order_dependent!
-
class << self
-
undef_method :test_order if method_defined? :test_order
-
define_method :test_order do :alpha end
-
end
-
end
-
-
##
-
# Make diffs for this Test use #pretty_inspect so that diff
-
# in assert_equal can have more details. NOTE: this is much slower
-
# than the regular inspect but much more usable for complex
-
# objects.
-
-
2
def self.make_my_diffs_pretty!
-
require "pp"
-
-
define_method :mu_pp do |o|
-
o.pretty_inspect
-
end
-
end
-
-
##
-
# Call this at the top of your tests when you want to run your
-
# tests in parallel. In doing so, you're admitting that you rule
-
# and your tests are awesome.
-
-
2
def self.parallelize_me!
-
include Minitest::Parallel::Test
-
extend Minitest::Parallel::Test::ClassMethods
-
end
-
-
##
-
# Returns all instance methods starting with "test_". Based on
-
# #test_order, the methods are either sorted, randomized
-
# (default), or run in parallel.
-
-
2
def self.runnable_methods
-
22
methods = methods_matching(/^test_/)
-
-
22
case self.test_order
-
when :random, :parallel then
-
22
max = methods.size
-
61
methods.sort.sort_by { rand max }
-
when :alpha, :sorted then
-
methods.sort
-
else
-
raise "Unknown test_order: #{self.test_order.inspect}"
-
end
-
end
-
-
##
-
# Defines the order to run tests (:random by default). Override
-
# this or use a convenience method to change it for your tests.
-
-
2
def self.test_order
-
8
:random
-
end
-
-
##
-
# The time it took to run this test.
-
-
2
attr_accessor :time
-
-
2
def marshal_dump # :nodoc:
-
super << self.time
-
end
-
-
2
def marshal_load ary # :nodoc:
-
self.time = ary.pop
-
super
-
end
-
-
2
TEARDOWN_METHODS = %w[ before_teardown teardown after_teardown ] # :nodoc:
-
-
##
-
# Runs a single test with setup/teardown hooks.
-
-
2
def run
-
39
with_info_handler do
-
39
time_it do
-
39
capture_exceptions do
-
39
before_setup; setup; after_setup
-
-
37
self.send self.name
-
end
-
-
39
TEARDOWN_METHODS.each do |hook|
-
117
capture_exceptions do
-
117
self.send hook
-
end
-
end
-
end
-
end
-
-
39
self # per contract
-
end
-
-
##
-
# Provides before/after hooks for setup and teardown. These are
-
# meant for library writers, NOT for regular test authors. See
-
# #before_setup for an example.
-
-
2
module LifecycleHooks
-
-
##
-
# Runs before every test, before setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# As a simplistic example:
-
#
-
# module MyMinitestPlugin
-
# def before_setup
-
# super
-
# # ... stuff to do before setup is run
-
# end
-
#
-
# def after_setup
-
# # ... stuff to do after setup is run
-
# super
-
# end
-
#
-
# def before_teardown
-
# super
-
# # ... stuff to do before teardown is run
-
# end
-
#
-
# def after_teardown
-
# # ... stuff to do after teardown is run
-
# super
-
# end
-
# end
-
#
-
# class MiniTest::Test
-
# include MyMinitestPlugin
-
# end
-
-
2
def before_setup; end
-
-
##
-
# Runs before every test. Use this to set up before each test
-
# run.
-
-
2
def setup; end
-
-
##
-
# Runs before every test, after setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
2
def after_setup; end
-
-
##
-
# Runs after every test, before teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
2
def before_teardown; end
-
-
##
-
# Runs after every test. Use this to clean up after each test
-
# run.
-
-
2
def teardown; end
-
-
##
-
# Runs after every test, after teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
2
def after_teardown; end
-
end # LifecycleHooks
-
-
2
def capture_exceptions # :nodoc:
-
156
yield
-
rescue *PASSTHROUGH_EXCEPTIONS
-
raise
-
rescue Assertion => e
-
3
self.failures << e
-
rescue Exception => e
-
4
self.failures << UnexpectedError.new(e)
-
end
-
-
##
-
# Did this run error?
-
-
2
def error?
-
14
self.failures.any? { |f| UnexpectedError === f }
-
end
-
-
##
-
# The location identifier of this test.
-
-
2
def location
-
7
loc = " [#{self.failure.location}]" unless passed? or error?
-
7
"#{self.class}##{self.name}#{loc}"
-
end
-
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
2
def passed?
-
53
not self.failure
-
end
-
-
##
-
# Returns ".", "F", or "E" based on the result of the run.
-
-
2
def result_code
-
39
self.failure and self.failure.result_code or "."
-
end
-
-
##
-
# Was this run skipped?
-
-
2
def skipped?
-
48
self.failure and Skip === self.failure
-
end
-
-
2
def time_it # :nodoc:
-
39
t0 = Minitest.clock_time
-
-
39
yield
-
ensure
-
39
self.time = Minitest.clock_time - t0
-
end
-
-
2
def to_s # :nodoc:
-
7
return location if passed? and not skipped?
-
-
7
failures.map { |failure|
-
7
"#{failure.result_label}:\n#{self.location}:\n#{failure.message}\n"
-
}.join "\n"
-
end
-
-
2
def with_info_handler &block # :nodoc:
-
39
t0 = Minitest.clock_time
-
-
39
handler = lambda do
-
warn "\nCurrent: %s#%s %.2fs" % [self.class, self.name, Minitest.clock_time - t0]
-
end
-
-
39
self.class.on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
2
include LifecycleHooks
-
2
include Guard
-
2
extend Guard
-
end # Test
-
end
-
-
2
require "minitest/unit" unless defined?(MiniTest) # compatibility layer only
-
# :stopdoc:
-
-
2
unless defined?(Minitest) then
-
# all of this crap is just to avoid circular requires and is only
-
# needed if a user requires "minitest/unit" directly instead of
-
# "minitest/autorun", so we also warn
-
-
from = caller.reject { |s| s =~ /rubygems/ }.join("\n ")
-
warn "Warning: you should require 'minitest/autorun' instead."
-
warn %(Warning: or add 'gem "minitest"' before 'require "minitest/autorun"')
-
warn "From:\n #{from}"
-
-
module Minitest; end
-
MiniTest = Minitest # prevents minitest.rb from requiring back to us
-
require "minitest"
-
end
-
-
2
MiniTest = Minitest unless defined?(MiniTest)
-
-
2
module Minitest
-
2
class Unit
-
2
VERSION = Minitest::VERSION
-
2
class TestCase < Minitest::Test
-
2
def self.inherited klass # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit::TestCase is now Minitest::Test. From #{from}"
-
super
-
end
-
end
-
-
2
def self.autorun # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.autorun is now Minitest.autorun. From #{from}"
-
Minitest.autorun
-
end
-
-
2
def self.after_tests(&b) # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.after_tests is now Minitest.after_run. From #{from}"
-
Minitest.after_run(&b)
-
end
-
end
-
end
-
-
# :startdoc:
-
2
require 'rack/utils'
-
-
2
module Rack
-
-
# Middleware that applies chunked transfer encoding to response bodies
-
# when the response does not include a Content-Length header.
-
2
class Chunked
-
2
include Rack::Utils
-
-
# A body wrapper that emits chunked responses
-
2
class Body
-
2
TERM = "\r\n"
-
2
TAIL = "0#{TERM}#{TERM}"
-
-
2
include Rack::Utils
-
-
2
def initialize(body)
-
@body = body
-
end
-
-
2
def each
-
term = TERM
-
@body.each do |chunk|
-
size = bytesize(chunk)
-
next if size == 0
-
-
chunk = chunk.dup.force_encoding(Encoding::BINARY) if chunk.respond_to?(:force_encoding)
-
yield [size.to_s(16), term, chunk, term].join
-
end
-
yield TAIL
-
end
-
-
2
def close
-
@body.close if @body.respond_to?(:close)
-
end
-
end
-
-
2
def initialize(app)
-
@app = app
-
end
-
-
# pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
-
# a version (nor response headers)
-
2
def chunkable_version?(ver)
-
case ver
-
when "HTTP/1.0", nil, "HTTP/0.9"
-
false
-
else
-
true
-
end
-
end
-
-
2
def call(env)
-
status, headers, body = @app.call(env)
-
headers = HeaderHash.new(headers)
-
-
if ! chunkable_version?(env['HTTP_VERSION']) ||
-
STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
-
headers[CONTENT_LENGTH] ||
-
headers['Transfer-Encoding']
-
[status, headers, body]
-
else
-
headers.delete(CONTENT_LENGTH)
-
headers['Transfer-Encoding'] = 'chunked'
-
[status, headers, Body.new(body)]
-
end
-
end
-
end
-
end
-
2
require 'rack/utils'
-
-
2
module Rack
-
-
# Middleware that enables conditional GET using If-None-Match and
-
# If-Modified-Since. The application should set either or both of the
-
# Last-Modified or Etag response headers according to RFC 2616. When
-
# either of the conditions is met, the response body is set to be zero
-
# length and the response status is set to 304 Not Modified.
-
#
-
# Applications that defer response body generation until the body's each
-
# message is received will avoid response body generation completely when
-
# a conditional GET matches.
-
#
-
# Adapted from Michael Klishin's Merb implementation:
-
# https://github.com/wycats/merb/blob/master/merb-core/lib/merb-core/rack/middleware/conditional_get.rb
-
2
class ConditionalGet
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
case env[REQUEST_METHOD]
-
when "GET", "HEAD"
-
status, headers, body = @app.call(env)
-
headers = Utils::HeaderHash.new(headers)
-
if status == 200 && fresh?(env, headers)
-
status = 304
-
headers.delete(CONTENT_TYPE)
-
headers.delete(CONTENT_LENGTH)
-
original_body = body
-
body = Rack::BodyProxy.new([]) do
-
original_body.close if original_body.respond_to?(:close)
-
end
-
end
-
[status, headers, body]
-
else
-
@app.call(env)
-
end
-
end
-
-
2
private
-
-
2
def fresh?(env, headers)
-
modified_since = env['HTTP_IF_MODIFIED_SINCE']
-
none_match = env['HTTP_IF_NONE_MATCH']
-
-
return false unless modified_since || none_match
-
-
success = true
-
success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
-
success &&= etag_matches?(none_match, headers) if none_match
-
success
-
end
-
-
2
def etag_matches?(none_match, headers)
-
etag = headers['ETag'] and etag == none_match
-
end
-
-
2
def modified_since?(modified_since, headers)
-
last_modified = to_rfc2822(headers['Last-Modified']) and
-
modified_since and
-
modified_since >= last_modified
-
end
-
-
2
def to_rfc2822(since)
-
# shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
-
# anything shorter is invalid, this avoids exceptions for common cases
-
# most common being the empty string
-
if since && since.length >= 16
-
# NOTE: there is no trivial way to write this in a non execption way
-
# _rfc2822 returns a hash but is not that usable
-
Time.rfc2822(since) rescue nil
-
else
-
nil
-
end
-
end
-
end
-
end
-
2
require 'digest/md5'
-
-
2
module Rack
-
# Automatically sets the ETag header on all String bodies.
-
#
-
# The ETag header is skipped if ETag or Last-Modified headers are sent or if
-
# a sendfile body (body.responds_to :to_path) is given (since such cases
-
# should be handled by apache/nginx).
-
#
-
# On initialization, you can pass two parameters: a Cache-Control directive
-
# used when Etag is absent and a directive when it is present. The first
-
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
-
2
class ETag
-
2
ETAG_STRING = 'ETag'.freeze
-
2
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
-
-
2
def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
-
2
@app = app
-
2
@cache_control = cache_control
-
2
@no_cache_control = no_cache_control
-
end
-
-
2
def call(env)
-
status, headers, body = @app.call(env)
-
-
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
-
original_body = body
-
digest, new_body = digest_body(body)
-
body = Rack::BodyProxy.new(new_body) do
-
original_body.close if original_body.respond_to?(:close)
-
end
-
headers[ETAG_STRING] = %(W/"#{digest}") if digest
-
end
-
-
unless headers[CACHE_CONTROL]
-
if digest
-
headers[CACHE_CONTROL] = @cache_control if @cache_control
-
else
-
headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
-
end
-
end
-
-
[status, headers, body]
-
end
-
-
2
private
-
-
2
def etag_status?(status)
-
status == 200 || status == 201
-
end
-
-
2
def etag_body?(body)
-
!body.respond_to?(:to_path)
-
end
-
-
2
def skip_caching?(headers)
-
(headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
-
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
-
end
-
-
2
def digest_body(body)
-
parts = []
-
digest = nil
-
-
body.each do |part|
-
parts << part
-
(digest ||= Digest::MD5.new) << part unless part.empty?
-
end
-
-
[digest && digest.hexdigest, parts]
-
end
-
end
-
end
-
2
require 'time'
-
2
require 'rack/utils'
-
2
require 'rack/mime'
-
-
2
module Rack
-
# Rack::File serves files below the +root+ directory given, according to the
-
# path info of the Rack request.
-
# e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
-
# as http://localhost:9292/passwd
-
#
-
# Handlers can detect if bodies are a Rack::File, and use mechanisms
-
# like sendfile on the +path+.
-
-
2
class File
-
2
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
-
2
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
-
-
2
attr_accessor :root
-
2
attr_accessor :path
-
2
attr_accessor :cache_control
-
-
2
alias :to_path :path
-
-
2
def initialize(root, headers={}, default_mime = 'text/plain')
-
2
@root = root
-
2
@headers = headers
-
2
@default_mime = default_mime
-
end
-
-
2
def call(env)
-
dup._call(env)
-
end
-
-
2
F = ::File
-
-
2
def _call(env)
-
unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
-
return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
-
end
-
-
path_info = Utils.unescape(env[PATH_INFO])
-
clean_path_info = Utils.clean_path_info(path_info)
-
-
@path = F.join(@root, clean_path_info)
-
-
available = begin
-
F.file?(@path) && F.readable?(@path)
-
rescue SystemCallError
-
false
-
end
-
-
if available
-
serving(env)
-
else
-
fail(404, "File not found: #{path_info}")
-
end
-
end
-
-
2
def serving(env)
-
if env["REQUEST_METHOD"] == "OPTIONS"
-
return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
-
end
-
last_modified = F.mtime(@path).httpdate
-
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
-
-
headers = { "Last-Modified" => last_modified }
-
headers[CONTENT_TYPE] = mime_type if mime_type
-
-
# Set custom headers
-
@headers.each { |field, content| headers[field] = content } if @headers
-
-
response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
-
-
size = filesize
-
-
ranges = Rack::Utils.byte_ranges(env, size)
-
if ranges.nil? || ranges.length > 1
-
# No ranges, or multiple ranges (which we don't support):
-
# TODO: Support multiple byte-ranges
-
response[0] = 200
-
@range = 0..size-1
-
elsif ranges.empty?
-
# Unsatisfiable. Return error, and file size:
-
response = fail(416, "Byte range unsatisfiable")
-
response[1]["Content-Range"] = "bytes */#{size}"
-
return response
-
else
-
# Partial content:
-
@range = ranges[0]
-
response[0] = 206
-
response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
-
size = @range.end - @range.begin + 1
-
end
-
-
response[2] = [response_body] unless response_body.nil?
-
-
response[1][CONTENT_LENGTH] = size.to_s
-
response
-
end
-
-
2
def each
-
F.open(@path, "rb") do |file|
-
file.seek(@range.begin)
-
remaining_len = @range.end-@range.begin+1
-
while remaining_len > 0
-
part = file.read([8192, remaining_len].min)
-
break unless part
-
remaining_len -= part.length
-
-
yield part
-
end
-
end
-
end
-
-
2
private
-
-
2
def fail(status, body, headers = {})
-
body += "\n"
-
[
-
status,
-
{
-
CONTENT_TYPE => "text/plain",
-
CONTENT_LENGTH => body.size.to_s,
-
"X-Cascade" => "pass"
-
}.merge!(headers),
-
[body]
-
]
-
end
-
-
# The MIME type for the contents of the file located at @path
-
2
def mime_type
-
Mime.mime_type(F.extname(@path), @default_mime)
-
end
-
-
2
def filesize
-
# If response_body is present, use its size.
-
return Rack::Utils.bytesize(response_body) if response_body
-
-
# We check via File::size? whether this file provides size info
-
# via stat (e.g. /proc files often don't), otherwise we have to
-
# figure it out by reading the whole file into memory.
-
F.size?(@path) || Utils.bytesize(F.read(@path))
-
end
-
-
# By default, the response body for file requests is nil.
-
# In this case, the response body will be generated later
-
# from the file at @path
-
2
def response_body
-
nil
-
end
-
end
-
end
-
2
require 'rack/body_proxy'
-
-
2
module Rack
-
-
2
class Head
-
# Rack::Head returns an empty body for all HEAD requests. It leaves
-
# all other requests unchanged.
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
status, headers, body = @app.call(env)
-
-
if env[REQUEST_METHOD] == HEAD
-
[
-
status, headers, Rack::BodyProxy.new([]) do
-
body.close if body.respond_to? :close
-
end
-
]
-
else
-
[status, headers, body]
-
end
-
end
-
end
-
-
end
-
2
require 'rack/utils'
-
2
require 'forwardable'
-
-
2
module Rack
-
# Rack::Lint validates your application and the requests and
-
# responses according to the Rack spec.
-
-
2
class Lint
-
2
def initialize(app)
-
@app = app
-
@content_length = nil
-
end
-
-
# :stopdoc:
-
-
2
class LintError < RuntimeError; end
-
2
module Assertion
-
2
def assert(message, &block)
-
unless block.call
-
raise LintError, message
-
end
-
end
-
end
-
2
include Assertion
-
-
## This specification aims to formalize the Rack protocol. You
-
## can (and should) use Rack::Lint to enforce it.
-
##
-
## When you develop middleware, be sure to add a Lint before and
-
## after to catch all mistakes.
-
-
## = Rack applications
-
-
## A Rack application is a Ruby object (not a class) that
-
## responds to +call+.
-
2
def call(env=nil)
-
dup._call(env)
-
end
-
-
2
def _call(env)
-
## It takes exactly one argument, the *environment*
-
assert("No env given") { env }
-
check_env env
-
-
env['rack.input'] = InputWrapper.new(env['rack.input'])
-
env['rack.errors'] = ErrorWrapper.new(env['rack.errors'])
-
-
## and returns an Array of exactly three values:
-
status, headers, @body = @app.call(env)
-
## The *status*,
-
check_status status
-
## the *headers*,
-
check_headers headers
-
-
check_hijack_response headers, env
-
-
## and the *body*.
-
check_content_type status, headers
-
check_content_length status, headers
-
@head_request = env[REQUEST_METHOD] == "HEAD"
-
[status, headers, self]
-
end
-
-
## == The Environment
-
2
def check_env(env)
-
## The environment must be an instance of Hash that includes
-
## CGI-like headers. The application is free to modify the
-
## environment.
-
assert("env #{env.inspect} is not a Hash, but #{env.class}") {
-
env.kind_of? Hash
-
}
-
-
##
-
## The environment is required to include these variables
-
## (adopted from PEP333), except when they'd be empty, but see
-
## below.
-
-
## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
-
## "GET" or "POST". This cannot ever
-
## be an empty string, and so is
-
## always required.
-
-
## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
-
## URL's "path" that corresponds to the
-
## application object, so that the
-
## application knows its virtual
-
## "location". This may be an empty
-
## string, if the application corresponds
-
## to the "root" of the server.
-
-
## <tt>PATH_INFO</tt>:: The remainder of the request URL's
-
## "path", designating the virtual
-
## "location" of the request's target
-
## within the application. This may be an
-
## empty string, if the request URL targets
-
## the application root and does not have a
-
## trailing slash. This value may be
-
## percent-encoded when I originating from
-
## a URL.
-
-
## <tt>QUERY_STRING</tt>:: The portion of the request URL that
-
## follows the <tt>?</tt>, if any. May be
-
## empty, but is always required!
-
-
## <tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>::
-
## When combined with <tt>SCRIPT_NAME</tt> and
-
## <tt>PATH_INFO</tt>, these variables can be
-
## used to complete the URL. Note, however,
-
## that <tt>HTTP_HOST</tt>, if present,
-
## should be used in preference to
-
## <tt>SERVER_NAME</tt> for reconstructing
-
## the request URL.
-
## <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt>
-
## can never be empty strings, and so
-
## are always required.
-
-
## <tt>HTTP_</tt> Variables:: Variables corresponding to the
-
## client-supplied HTTP request
-
## headers (i.e., variables whose
-
## names begin with <tt>HTTP_</tt>). The
-
## presence or absence of these
-
## variables should correspond with
-
## the presence or absence of the
-
## appropriate HTTP header in the
-
## request. See
-
## <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18">
-
## RFC3875 section 4.1.18</a> for
-
## specific behavior.
-
-
## In addition to this, the Rack environment must include these
-
## Rack-specific variables:
-
-
## <tt>rack.version</tt>:: The Array representing this version of Rack
-
## See Rack::VERSION, that corresponds to
-
## the version of this SPEC.
-
-
## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
-
## request URL.
-
-
## <tt>rack.input</tt>:: See below, the input stream.
-
-
## <tt>rack.errors</tt>:: See below, the error stream.
-
-
## <tt>rack.multithread</tt>:: true if the application object may be
-
## simultaneously invoked by another thread
-
## in the same process, false otherwise.
-
-
## <tt>rack.multiprocess</tt>:: true if an equivalent application object
-
## may be simultaneously invoked by another
-
## process, false otherwise.
-
-
## <tt>rack.run_once</tt>:: true if the server expects
-
## (but does not guarantee!) that the
-
## application will only be invoked this one
-
## time during the life of its containing
-
## process. Normally, this will only be true
-
## for a server based on CGI
-
## (or something similar).
-
-
## <tt>rack.hijack?</tt>:: present and true if the server supports
-
## connection hijacking. See below, hijacking.
-
-
## <tt>rack.hijack</tt>:: an object responding to #call that must be
-
## called at least once before using
-
## rack.hijack_io.
-
## It is recommended #call return rack.hijack_io
-
## as well as setting it in env if necessary.
-
-
## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack
-
## has received #call, this will contain
-
## an object resembling an IO. See hijacking.
-
-
## Additional environment specifications have approved to
-
## standardized middleware APIs. None of these are required to
-
## be implemented by the server.
-
-
## <tt>rack.session</tt>:: A hash like interface for storing
-
## request session data.
-
## The store must implement:
-
if session = env['rack.session']
-
## store(key, value) (aliased as []=);
-
assert("session #{session.inspect} must respond to store and []=") {
-
session.respond_to?(:store) && session.respond_to?(:[]=)
-
}
-
-
## fetch(key, default = nil) (aliased as []);
-
assert("session #{session.inspect} must respond to fetch and []") {
-
session.respond_to?(:fetch) && session.respond_to?(:[])
-
}
-
-
## delete(key);
-
assert("session #{session.inspect} must respond to delete") {
-
session.respond_to?(:delete)
-
}
-
-
## clear;
-
assert("session #{session.inspect} must respond to clear") {
-
session.respond_to?(:clear)
-
}
-
end
-
-
## <tt>rack.logger</tt>:: A common object interface for logging messages.
-
## The object must implement:
-
if logger = env['rack.logger']
-
## info(message, &block)
-
assert("logger #{logger.inspect} must respond to info") {
-
logger.respond_to?(:info)
-
}
-
-
## debug(message, &block)
-
assert("logger #{logger.inspect} must respond to debug") {
-
logger.respond_to?(:debug)
-
}
-
-
## warn(message, &block)
-
assert("logger #{logger.inspect} must respond to warn") {
-
logger.respond_to?(:warn)
-
}
-
-
## error(message, &block)
-
assert("logger #{logger.inspect} must respond to error") {
-
logger.respond_to?(:error)
-
}
-
-
## fatal(message, &block)
-
assert("logger #{logger.inspect} must respond to fatal") {
-
logger.respond_to?(:fatal)
-
}
-
end
-
-
## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
-
if bufsize = env['rack.multipart.buffer_size']
-
assert("rack.multipart.buffer_size must be an Integer > 0 if specified") {
-
bufsize.is_a?(Integer) && bufsize > 0
-
}
-
end
-
-
## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
-
if tempfile_factory = env['rack.multipart.tempfile_factory']
-
assert("rack.multipart.tempfile_factory must respond to #call") { tempfile_factory.respond_to?(:call) }
-
env['rack.multipart.tempfile_factory'] = lambda do |filename, content_type|
-
io = tempfile_factory.call(filename, content_type)
-
assert("rack.multipart.tempfile_factory return value must respond to #<<") { io.respond_to?(:<<) }
-
io
-
end
-
end
-
-
## The server or the application can store their own data in the
-
## environment, too. The keys must contain at least one dot,
-
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
-
## is reserved for use with the Rack core distribution and other
-
## accepted specifications and must not be used otherwise.
-
##
-
-
%w[REQUEST_METHOD SERVER_NAME SERVER_PORT
-
QUERY_STRING
-
rack.version rack.input rack.errors
-
rack.multithread rack.multiprocess rack.run_once].each { |header|
-
assert("env missing required key #{header}") { env.include? header }
-
}
-
-
## The environment must not contain the keys
-
## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
-
## (use the versions without <tt>HTTP_</tt>).
-
%w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
-
assert("env contains #{header}, must use #{header[5,-1]}") {
-
not env.include? header
-
}
-
}
-
-
## The CGI keys (named without a period) must have String values.
-
env.each { |key, value|
-
next if key.include? "." # Skip extensions
-
assert("env variable #{key} has non-string value #{value.inspect}") {
-
value.kind_of? String
-
}
-
}
-
-
## There are the following restrictions:
-
-
## * <tt>rack.version</tt> must be an array of Integers.
-
assert("rack.version must be an Array, was #{env["rack.version"].class}") {
-
env["rack.version"].kind_of? Array
-
}
-
## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
-
assert("rack.url_scheme unknown: #{env["rack.url_scheme"].inspect}") {
-
%w[http https].include? env["rack.url_scheme"]
-
}
-
-
## * There must be a valid input stream in <tt>rack.input</tt>.
-
check_input env["rack.input"]
-
## * There must be a valid error stream in <tt>rack.errors</tt>.
-
check_error env["rack.errors"]
-
## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
-
check_hijack env
-
-
## * The <tt>REQUEST_METHOD</tt> must be a valid token.
-
assert("REQUEST_METHOD unknown: #{env[REQUEST_METHOD]}") {
-
env["REQUEST_METHOD"] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
-
}
-
-
## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
-
assert("SCRIPT_NAME must start with /") {
-
!env.include?("SCRIPT_NAME") ||
-
env["SCRIPT_NAME"] == "" ||
-
env["SCRIPT_NAME"] =~ /\A\//
-
}
-
## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
-
assert("PATH_INFO must start with /") {
-
!env.include?("PATH_INFO") ||
-
env["PATH_INFO"] == "" ||
-
env["PATH_INFO"] =~ /\A\//
-
}
-
## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
-
assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") {
-
!env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/
-
}
-
-
## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
-
## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
-
## <tt>SCRIPT_NAME</tt> is empty.
-
assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") {
-
env["SCRIPT_NAME"] || env["PATH_INFO"]
-
}
-
## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
-
assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") {
-
env["SCRIPT_NAME"] != "/"
-
}
-
end
-
-
## === The Input Stream
-
##
-
## The input stream is an IO-like object which contains the raw HTTP
-
## POST data.
-
2
def check_input(input)
-
## When applicable, its external encoding must be "ASCII-8BIT" and it
-
## must be opened in binary mode, for Ruby 1.9 compatibility.
-
assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
-
input.external_encoding.name == "ASCII-8BIT"
-
} if input.respond_to?(:external_encoding)
-
assert("rack.input #{input} is not opened in binary mode") {
-
input.binmode?
-
} if input.respond_to?(:binmode?)
-
-
## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
-
[:gets, :each, :read, :rewind].each { |method|
-
assert("rack.input #{input} does not respond to ##{method}") {
-
input.respond_to? method
-
}
-
}
-
end
-
-
2
class InputWrapper
-
2
include Assertion
-
-
2
def initialize(input)
-
@input = input
-
end
-
-
## * +gets+ must be called without arguments and return a string,
-
## or +nil+ on EOF.
-
2
def gets(*args)
-
assert("rack.input#gets called with arguments") { args.size == 0 }
-
v = @input.gets
-
assert("rack.input#gets didn't return a String") {
-
v.nil? or v.kind_of? String
-
}
-
v
-
end
-
-
## * +read+ behaves like IO#read.
-
## Its signature is <tt>read([length, [buffer]])</tt>.
-
##
-
## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
-
## and +buffer+ must be a String and may not be nil.
-
##
-
## If +length+ is given and not nil, then this method reads at most
-
## +length+ bytes from the input stream.
-
##
-
## If +length+ is not given or nil, then this method reads
-
## all data until EOF.
-
##
-
## When EOF is reached, this method returns nil if +length+ is given
-
## and not nil, or "" if +length+ is not given or is nil.
-
##
-
## If +buffer+ is given, then the read data will be placed
-
## into +buffer+ instead of a newly created String object.
-
2
def read(*args)
-
assert("rack.input#read called with too many arguments") {
-
args.size <= 2
-
}
-
if args.size >= 1
-
assert("rack.input#read called with non-integer and non-nil length") {
-
args.first.kind_of?(Integer) || args.first.nil?
-
}
-
assert("rack.input#read called with a negative length") {
-
args.first.nil? || args.first >= 0
-
}
-
end
-
if args.size >= 2
-
assert("rack.input#read called with non-String buffer") {
-
args[1].kind_of?(String)
-
}
-
end
-
-
v = @input.read(*args)
-
-
assert("rack.input#read didn't return nil or a String") {
-
v.nil? or v.kind_of? String
-
}
-
if args[0].nil?
-
assert("rack.input#read(nil) returned nil on EOF") {
-
!v.nil?
-
}
-
end
-
-
v
-
end
-
-
## * +each+ must be called without arguments and only yield Strings.
-
2
def each(*args)
-
assert("rack.input#each called with arguments") { args.size == 0 }
-
@input.each { |line|
-
assert("rack.input#each didn't yield a String") {
-
line.kind_of? String
-
}
-
yield line
-
}
-
end
-
-
## * +rewind+ must be called without arguments. It rewinds the input
-
## stream back to the beginning. It must not raise Errno::ESPIPE:
-
## that is, it may not be a pipe or a socket. Therefore, handler
-
## developers must buffer the input data into some rewindable object
-
## if the underlying input stream is not rewindable.
-
2
def rewind(*args)
-
assert("rack.input#rewind called with arguments") { args.size == 0 }
-
assert("rack.input#rewind raised Errno::ESPIPE") {
-
begin
-
@input.rewind
-
true
-
rescue Errno::ESPIPE
-
false
-
end
-
}
-
end
-
-
## * +close+ must never be called on the input stream.
-
2
def close(*args)
-
assert("rack.input#close must not be called") { false }
-
end
-
end
-
-
## === The Error Stream
-
2
def check_error(error)
-
## The error stream must respond to +puts+, +write+ and +flush+.
-
[:puts, :write, :flush].each { |method|
-
assert("rack.error #{error} does not respond to ##{method}") {
-
error.respond_to? method
-
}
-
}
-
end
-
-
2
class ErrorWrapper
-
2
include Assertion
-
-
2
def initialize(error)
-
@error = error
-
end
-
-
## * +puts+ must be called with a single argument that responds to +to_s+.
-
2
def puts(str)
-
@error.puts str
-
end
-
-
## * +write+ must be called with a single argument that is a String.
-
2
def write(str)
-
assert("rack.errors#write not called with a String") { str.kind_of? String }
-
@error.write str
-
end
-
-
## * +flush+ must be called without arguments and must be called
-
## in order to make the error appear for sure.
-
2
def flush
-
@error.flush
-
end
-
-
## * +close+ must never be called on the error stream.
-
2
def close(*args)
-
assert("rack.errors#close must not be called") { false }
-
end
-
end
-
-
2
class HijackWrapper
-
2
include Assertion
-
2
extend Forwardable
-
-
2
REQUIRED_METHODS = [
-
:read, :write, :read_nonblock, :write_nonblock, :flush, :close,
-
:close_read, :close_write, :closed?
-
]
-
-
2
def_delegators :@io, *REQUIRED_METHODS
-
-
2
def initialize(io)
-
@io = io
-
REQUIRED_METHODS.each do |meth|
-
assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth }
-
end
-
end
-
end
-
-
## === Hijacking
-
#
-
# AUTHORS: n.b. The trailing whitespace between paragraphs is important and
-
# should not be removed. The whitespace creates paragraphs in the RDoc
-
# output.
-
#
-
## ==== Request (before status)
-
2
def check_hijack(env)
-
if env['rack.hijack?']
-
## If rack.hijack? is true then rack.hijack must respond to #call.
-
original_hijack = env['rack.hijack']
-
assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) }
-
env['rack.hijack'] = proc do
-
## rack.hijack must return the io that will also be assigned (or is
-
## already present, in rack.hijack_io.
-
io = original_hijack.call
-
HijackWrapper.new(io)
-
##
-
## rack.hijack_io must respond to:
-
## <tt>read, write, read_nonblock, write_nonblock, flush, close,
-
## close_read, close_write, closed?</tt>
-
##
-
## The semantics of these IO methods must be a best effort match to
-
## those of a normal ruby IO or Socket object, using standard
-
## arguments and raising standard exceptions. Servers are encouraged
-
## to simply pass on real IO objects, although it is recognized that
-
## this approach is not directly compatible with SPDY and HTTP 2.0.
-
##
-
## IO provided in rack.hijack_io should preference the
-
## IO::WaitReadable and IO::WaitWritable APIs wherever supported.
-
##
-
## There is a deliberate lack of full specification around
-
## rack.hijack_io, as semantics will change from server to server.
-
## Users are encouraged to utilize this API with a knowledge of their
-
## server choice, and servers may extend the functionality of
-
## hijack_io to provide additional features to users. The purpose of
-
## rack.hijack is for Rack to "get out of the way", as such, Rack only
-
## provides the minimum of specification and support.
-
env['rack.hijack_io'] = HijackWrapper.new(env['rack.hijack_io'])
-
io
-
end
-
else
-
##
-
## If rack.hijack? is false, then rack.hijack should not be set.
-
assert("rack.hijack? is false, but rack.hijack is present") { env['rack.hijack'].nil? }
-
##
-
## If rack.hijack? is false, then rack.hijack_io should not be set.
-
assert("rack.hijack? is false, but rack.hijack_io is present") { env['rack.hijack_io'].nil? }
-
end
-
end
-
-
## ==== Response (after headers)
-
## It is also possible to hijack a response after the status and headers
-
## have been sent.
-
2
def check_hijack_response(headers, env)
-
-
# this check uses headers like a hash, but the spec only requires
-
# headers respond to #each
-
headers = Rack::Utils::HeaderHash.new(headers)
-
-
## In order to do this, an application may set the special header
-
## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
-
## accepting an argument that conforms to the <tt>rack.hijack_io</tt>
-
## protocol.
-
##
-
## After the headers have been sent, and this hijack callback has been
-
## called, the application is now responsible for the remaining lifecycle
-
## of the IO. The application is also responsible for maintaining HTTP
-
## semantics. Of specific note, in almost all cases in the current SPEC,
-
## applications will have wanted to specify the header Connection:close in
-
## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
-
## returning hijacked sockets to the web server. For that purpose, use the
-
## body streaming API instead (progressively yielding strings via each).
-
##
-
## Servers must ignore the <tt>body</tt> part of the response tuple when
-
## the <tt>rack.hijack</tt> response API is in use.
-
-
if env['rack.hijack?'] && headers['rack.hijack']
-
assert('rack.hijack header must respond to #call') {
-
headers['rack.hijack'].respond_to? :call
-
}
-
original_hijack = headers['rack.hijack']
-
headers['rack.hijack'] = proc do |io|
-
original_hijack.call HijackWrapper.new(io)
-
end
-
else
-
##
-
## The special response header <tt>rack.hijack</tt> must only be set
-
## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
-
assert('rack.hijack header must not be present if server does not support hijacking') {
-
headers['rack.hijack'].nil?
-
}
-
end
-
end
-
## ==== Conventions
-
## * Middleware should not use hijack unless it is handling the whole
-
## response.
-
## * Middleware may wrap the IO object for the response pattern.
-
## * Middleware should not wrap the IO object for the request pattern. The
-
## request pattern is intended to provide the hijacker with "raw tcp".
-
-
## == The Response
-
-
## === The Status
-
2
def check_status(status)
-
## This is an HTTP status. When parsed as integer (+to_i+), it must be
-
## greater than or equal to 100.
-
assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
-
end
-
-
## === The Headers
-
2
def check_headers(header)
-
## The header must respond to +each+, and yield values of key and value.
-
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
-
header.respond_to? :each
-
}
-
header.each { |key, value|
-
## Special headers starting "rack." are for communicating with the
-
## server, and must not be sent back to the client.
-
next if key =~ /^rack\..+$/
-
-
## The header keys must be Strings.
-
assert("header key must be a string, was #{key.class}") {
-
key.kind_of? String
-
}
-
## The header must not contain a +Status+ key.
-
assert("header must not contain Status") { key.downcase != "status" }
-
## The header must conform to RFC7230 token specification, i.e. cannot
-
## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
-
assert("invalid header name: #{key}") { key !~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ }
-
-
## The values of the header must be Strings,
-
assert("a header value must be a String, but the value of " +
-
"'#{key}' is a #{value.class}") { value.kind_of? String }
-
## consisting of lines (for multiple header values, e.g. multiple
-
## <tt>Set-Cookie</tt> values) separated by "\\n".
-
value.split("\n").each { |item|
-
## The lines must not contain characters below 037.
-
assert("invalid header value #{key}: #{item.inspect}") {
-
item !~ /[\000-\037]/
-
}
-
}
-
}
-
end
-
-
## === The Content-Type
-
2
def check_content_type(status, headers)
-
headers.each { |key, value|
-
## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
-
## 204, 205 or 304.
-
if key.downcase == "content-type"
-
assert("Content-Type header found in #{status} response, not allowed") {
-
not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
-
}
-
return
-
end
-
}
-
end
-
-
## === The Content-Length
-
2
def check_content_length(status, headers)
-
headers.each { |key, value|
-
if key.downcase == 'content-length'
-
## There must not be a <tt>Content-Length</tt> header when the
-
## +Status+ is 1xx, 204, 205 or 304.
-
assert("Content-Length header found in #{status} response, not allowed") {
-
not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
-
}
-
@content_length = value
-
end
-
}
-
end
-
-
2
def verify_content_length(bytes)
-
if @head_request
-
assert("Response body was given for HEAD request, but should be empty") {
-
bytes == 0
-
}
-
elsif @content_length
-
assert("Content-Length header was #{@content_length}, but should be #{bytes}") {
-
@content_length == bytes.to_s
-
}
-
end
-
end
-
-
## === The Body
-
2
def each
-
@closed = false
-
bytes = 0
-
-
## The Body must respond to +each+
-
assert("Response body must respond to each") do
-
@body.respond_to?(:each)
-
end
-
-
@body.each { |part|
-
## and must only yield String values.
-
assert("Body yielded non-string value #{part.inspect}") {
-
part.kind_of? String
-
}
-
bytes += Rack::Utils.bytesize(part)
-
yield part
-
}
-
verify_content_length(bytes)
-
-
##
-
## The Body itself should not be an instance of String, as this will
-
## break in Ruby 1.9.
-
##
-
## If the Body responds to +close+, it will be called after iteration. If
-
## the body is replaced by a middleware after action, the original body
-
## must be closed first, if it responds to close.
-
# XXX howto: assert("Body has not been closed") { @closed }
-
-
-
##
-
## If the Body responds to +to_path+, it must return a String
-
## identifying the location of a file whose contents are identical
-
## to that produced by calling +each+; this may be used by the
-
## server as an alternative, possibly more efficient way to
-
## transport the response.
-
-
if @body.respond_to?(:to_path)
-
assert("The file identified by body.to_path does not exist") {
-
::File.exist? @body.to_path
-
}
-
end
-
-
##
-
## The Body commonly is an Array of Strings, the application
-
## instance itself, or a File-like object.
-
end
-
-
2
def close
-
@closed = true
-
@body.close if @body.respond_to?(:close)
-
end
-
-
# :startdoc:
-
-
end
-
end
-
-
## == Thanks
-
## Some parts of this specification are adopted from PEP333: Python
-
## Web Server Gateway Interface
-
## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
-
## everyone involved in that effort.
-
2
require 'thread'
-
2
require 'rack/body_proxy'
-
-
2
module Rack
-
# Rack::Lock locks every request inside a mutex, so that every request
-
# will effectively be executed synchronously.
-
2
class Lock
-
2
FLAG = 'rack.multithread'.freeze
-
-
2
def initialize(app, mutex = Mutex.new)
-
2
@app, @mutex = app, mutex
-
end
-
-
2
def call(env)
-
old, env[FLAG] = env[FLAG], false
-
@mutex.lock
-
response = @app.call(env)
-
body = BodyProxy.new(response[2]) { @mutex.unlock }
-
response[2] = body
-
response
-
ensure
-
@mutex.unlock unless body
-
env[FLAG] = old
-
end
-
end
-
end
-
2
module Rack
-
2
class MethodOverride
-
2
HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK)
-
-
2
METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
-
2
HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
-
2
ALLOWED_METHODS = ["POST"]
-
-
2
def initialize(app)
-
2
@app = app
-
end
-
-
2
def call(env)
-
if allowed_methods.include?(env[REQUEST_METHOD])
-
method = method_override(env)
-
if HTTP_METHODS.include?(method)
-
env["rack.methodoverride.original_method"] = env[REQUEST_METHOD]
-
env[REQUEST_METHOD] = method
-
end
-
end
-
-
@app.call(env)
-
end
-
-
2
def method_override(env)
-
req = Request.new(env)
-
method = method_override_param(req) ||
-
env[HTTP_METHOD_OVERRIDE_HEADER]
-
method.to_s.upcase
-
end
-
-
2
private
-
-
2
def allowed_methods
-
ALLOWED_METHODS
-
end
-
-
2
def method_override_param(req)
-
req.POST[METHOD_OVERRIDE_PARAM_KEY]
-
rescue Utils::InvalidParameterError, Utils::ParameterTypeError
-
end
-
end
-
end
-
2
module Rack
-
2
module Mime
-
# Returns String with mime type if found, otherwise use +fallback+.
-
# +ext+ should be filename extension in the '.ext' format that
-
# File.extname(file) returns.
-
# +fallback+ may be any object
-
#
-
# Also see the documentation for MIME_TYPES
-
#
-
# Usage:
-
# Rack::Mime.mime_type('.foo')
-
#
-
# This is a shortcut for:
-
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
-
-
2
def mime_type(ext, fallback='application/octet-stream')
-
MIME_TYPES.fetch(ext.to_s.downcase, fallback)
-
end
-
2
module_function :mime_type
-
-
# Returns true if the given value is a mime match for the given mime match
-
# specification, false otherwise.
-
#
-
# Rack::Mime.match?('text/html', 'text/*') => true
-
# Rack::Mime.match?('text/plain', '*') => true
-
# Rack::Mime.match?('text/html', 'application/json') => false
-
-
2
def match?(value, matcher)
-
v1, v2 = value.split('/', 2)
-
m1, m2 = matcher.split('/', 2)
-
-
(m1 == '*' || v1 == m1) && (m2.nil? || m2 == '*' || m2 == v2)
-
end
-
2
module_function :match?
-
-
# List of most common mime-types, selected various sources
-
# according to their usefulness in a webserving scope for Ruby
-
# users.
-
#
-
# To amend this list with your local mime.types list you can use:
-
#
-
# require 'webrick/httputils'
-
# list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types')
-
# Rack::Mime::MIME_TYPES.merge!(list)
-
#
-
# N.B. On Ubuntu the mime.types file does not include the leading period, so
-
# users may need to modify the data before merging into the hash.
-
#
-
# To add the list mongrel provides, use:
-
#
-
# require 'mongrel/handlers'
-
# Rack::Mime::MIME_TYPES.merge!(Mongrel::DirHandler::MIME_TYPES)
-
-
2
MIME_TYPES = {
-
".123" => "application/vnd.lotus-1-2-3",
-
".3dml" => "text/vnd.in3d.3dml",
-
".3g2" => "video/3gpp2",
-
".3gp" => "video/3gpp",
-
".a" => "application/octet-stream",
-
".acc" => "application/vnd.americandynamics.acc",
-
".ace" => "application/x-ace-compressed",
-
".acu" => "application/vnd.acucobol",
-
".aep" => "application/vnd.audiograph",
-
".afp" => "application/vnd.ibm.modcap",
-
".ai" => "application/postscript",
-
".aif" => "audio/x-aiff",
-
".aiff" => "audio/x-aiff",
-
".ami" => "application/vnd.amiga.ami",
-
".appcache" => "text/cache-manifest",
-
".apr" => "application/vnd.lotus-approach",
-
".asc" => "application/pgp-signature",
-
".asf" => "video/x-ms-asf",
-
".asm" => "text/x-asm",
-
".aso" => "application/vnd.accpac.simply.aso",
-
".asx" => "video/x-ms-asf",
-
".atc" => "application/vnd.acucorp",
-
".atom" => "application/atom+xml",
-
".atomcat" => "application/atomcat+xml",
-
".atomsvc" => "application/atomsvc+xml",
-
".atx" => "application/vnd.antix.game-component",
-
".au" => "audio/basic",
-
".avi" => "video/x-msvideo",
-
".bat" => "application/x-msdownload",
-
".bcpio" => "application/x-bcpio",
-
".bdm" => "application/vnd.syncml.dm+wbxml",
-
".bh2" => "application/vnd.fujitsu.oasysprs",
-
".bin" => "application/octet-stream",
-
".bmi" => "application/vnd.bmi",
-
".bmp" => "image/bmp",
-
".box" => "application/vnd.previewsystems.box",
-
".btif" => "image/prs.btif",
-
".bz" => "application/x-bzip",
-
".bz2" => "application/x-bzip2",
-
".c" => "text/x-c",
-
".c4g" => "application/vnd.clonk.c4group",
-
".cab" => "application/vnd.ms-cab-compressed",
-
".cc" => "text/x-c",
-
".ccxml" => "application/ccxml+xml",
-
".cdbcmsg" => "application/vnd.contact.cmsg",
-
".cdkey" => "application/vnd.mediastation.cdkey",
-
".cdx" => "chemical/x-cdx",
-
".cdxml" => "application/vnd.chemdraw+xml",
-
".cdy" => "application/vnd.cinderella",
-
".cer" => "application/pkix-cert",
-
".cgm" => "image/cgm",
-
".chat" => "application/x-chat",
-
".chm" => "application/vnd.ms-htmlhelp",
-
".chrt" => "application/vnd.kde.kchart",
-
".cif" => "chemical/x-cif",
-
".cii" => "application/vnd.anser-web-certificate-issue-initiation",
-
".cil" => "application/vnd.ms-artgalry",
-
".cla" => "application/vnd.claymore",
-
".class" => "application/octet-stream",
-
".clkk" => "application/vnd.crick.clicker.keyboard",
-
".clkp" => "application/vnd.crick.clicker.palette",
-
".clkt" => "application/vnd.crick.clicker.template",
-
".clkw" => "application/vnd.crick.clicker.wordbank",
-
".clkx" => "application/vnd.crick.clicker",
-
".clp" => "application/x-msclip",
-
".cmc" => "application/vnd.cosmocaller",
-
".cmdf" => "chemical/x-cmdf",
-
".cml" => "chemical/x-cml",
-
".cmp" => "application/vnd.yellowriver-custom-menu",
-
".cmx" => "image/x-cmx",
-
".com" => "application/x-msdownload",
-
".conf" => "text/plain",
-
".cpio" => "application/x-cpio",
-
".cpp" => "text/x-c",
-
".cpt" => "application/mac-compactpro",
-
".crd" => "application/x-mscardfile",
-
".crl" => "application/pkix-crl",
-
".crt" => "application/x-x509-ca-cert",
-
".csh" => "application/x-csh",
-
".csml" => "chemical/x-csml",
-
".csp" => "application/vnd.commonspace",
-
".css" => "text/css",
-
".csv" => "text/csv",
-
".curl" => "application/vnd.curl",
-
".cww" => "application/prs.cww",
-
".cxx" => "text/x-c",
-
".daf" => "application/vnd.mobius.daf",
-
".davmount" => "application/davmount+xml",
-
".dcr" => "application/x-director",
-
".dd2" => "application/vnd.oma.dd2+xml",
-
".ddd" => "application/vnd.fujixerox.ddd",
-
".deb" => "application/x-debian-package",
-
".der" => "application/x-x509-ca-cert",
-
".dfac" => "application/vnd.dreamfactory",
-
".diff" => "text/x-diff",
-
".dis" => "application/vnd.mobius.dis",
-
".djv" => "image/vnd.djvu",
-
".djvu" => "image/vnd.djvu",
-
".dll" => "application/x-msdownload",
-
".dmg" => "application/octet-stream",
-
".dna" => "application/vnd.dna",
-
".doc" => "application/msword",
-
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-
".dot" => "application/msword",
-
".dp" => "application/vnd.osgi.dp",
-
".dpg" => "application/vnd.dpgraph",
-
".dsc" => "text/prs.lines.tag",
-
".dtd" => "application/xml-dtd",
-
".dts" => "audio/vnd.dts",
-
".dtshd" => "audio/vnd.dts.hd",
-
".dv" => "video/x-dv",
-
".dvi" => "application/x-dvi",
-
".dwf" => "model/vnd.dwf",
-
".dwg" => "image/vnd.dwg",
-
".dxf" => "image/vnd.dxf",
-
".dxp" => "application/vnd.spotfire.dxp",
-
".ear" => "application/java-archive",
-
".ecelp4800" => "audio/vnd.nuera.ecelp4800",
-
".ecelp7470" => "audio/vnd.nuera.ecelp7470",
-
".ecelp9600" => "audio/vnd.nuera.ecelp9600",
-
".ecma" => "application/ecmascript",
-
".edm" => "application/vnd.novadigm.edm",
-
".edx" => "application/vnd.novadigm.edx",
-
".efif" => "application/vnd.picsel",
-
".ei6" => "application/vnd.pg.osasli",
-
".eml" => "message/rfc822",
-
".eol" => "audio/vnd.digital-winds",
-
".eot" => "application/vnd.ms-fontobject",
-
".eps" => "application/postscript",
-
".es3" => "application/vnd.eszigno3+xml",
-
".esf" => "application/vnd.epson.esf",
-
".etx" => "text/x-setext",
-
".exe" => "application/x-msdownload",
-
".ext" => "application/vnd.novadigm.ext",
-
".ez" => "application/andrew-inset",
-
".ez2" => "application/vnd.ezpix-album",
-
".ez3" => "application/vnd.ezpix-package",
-
".f" => "text/x-fortran",
-
".f77" => "text/x-fortran",
-
".f90" => "text/x-fortran",
-
".fbs" => "image/vnd.fastbidsheet",
-
".fdf" => "application/vnd.fdf",
-
".fe_launch" => "application/vnd.denovo.fcselayout-link",
-
".fg5" => "application/vnd.fujitsu.oasysgp",
-
".fli" => "video/x-fli",
-
".flo" => "application/vnd.micrografx.flo",
-
".flv" => "video/x-flv",
-
".flw" => "application/vnd.kde.kivio",
-
".flx" => "text/vnd.fmi.flexstor",
-
".fly" => "text/vnd.fly",
-
".fm" => "application/vnd.framemaker",
-
".fnc" => "application/vnd.frogans.fnc",
-
".for" => "text/x-fortran",
-
".fpx" => "image/vnd.fpx",
-
".fsc" => "application/vnd.fsc.weblaunch",
-
".fst" => "image/vnd.fst",
-
".ftc" => "application/vnd.fluxtime.clip",
-
".fti" => "application/vnd.anser-web-funds-transfer-initiation",
-
".fvt" => "video/vnd.fvt",
-
".fzs" => "application/vnd.fuzzysheet",
-
".g3" => "image/g3fax",
-
".gac" => "application/vnd.groove-account",
-
".gdl" => "model/vnd.gdl",
-
".gem" => "application/octet-stream",
-
".gemspec" => "text/x-script.ruby",
-
".ghf" => "application/vnd.groove-help",
-
".gif" => "image/gif",
-
".gim" => "application/vnd.groove-identity-message",
-
".gmx" => "application/vnd.gmx",
-
".gph" => "application/vnd.flographit",
-
".gqf" => "application/vnd.grafeq",
-
".gram" => "application/srgs",
-
".grv" => "application/vnd.groove-injector",
-
".grxml" => "application/srgs+xml",
-
".gtar" => "application/x-gtar",
-
".gtm" => "application/vnd.groove-tool-message",
-
".gtw" => "model/vnd.gtw",
-
".gv" => "text/vnd.graphviz",
-
".gz" => "application/x-gzip",
-
".h" => "text/x-c",
-
".h261" => "video/h261",
-
".h263" => "video/h263",
-
".h264" => "video/h264",
-
".hbci" => "application/vnd.hbci",
-
".hdf" => "application/x-hdf",
-
".hh" => "text/x-c",
-
".hlp" => "application/winhlp",
-
".hpgl" => "application/vnd.hp-hpgl",
-
".hpid" => "application/vnd.hp-hpid",
-
".hps" => "application/vnd.hp-hps",
-
".hqx" => "application/mac-binhex40",
-
".htc" => "text/x-component",
-
".htke" => "application/vnd.kenameaapp",
-
".htm" => "text/html",
-
".html" => "text/html",
-
".hvd" => "application/vnd.yamaha.hv-dic",
-
".hvp" => "application/vnd.yamaha.hv-voice",
-
".hvs" => "application/vnd.yamaha.hv-script",
-
".icc" => "application/vnd.iccprofile",
-
".ice" => "x-conference/x-cooltalk",
-
".ico" => "image/vnd.microsoft.icon",
-
".ics" => "text/calendar",
-
".ief" => "image/ief",
-
".ifb" => "text/calendar",
-
".ifm" => "application/vnd.shana.informed.formdata",
-
".igl" => "application/vnd.igloader",
-
".igs" => "model/iges",
-
".igx" => "application/vnd.micrografx.igx",
-
".iif" => "application/vnd.shana.informed.interchange",
-
".imp" => "application/vnd.accpac.simply.imp",
-
".ims" => "application/vnd.ms-ims",
-
".ipk" => "application/vnd.shana.informed.package",
-
".irm" => "application/vnd.ibm.rights-management",
-
".irp" => "application/vnd.irepository.package+xml",
-
".iso" => "application/octet-stream",
-
".itp" => "application/vnd.shana.informed.formtemplate",
-
".ivp" => "application/vnd.immervision-ivp",
-
".ivu" => "application/vnd.immervision-ivu",
-
".jad" => "text/vnd.sun.j2me.app-descriptor",
-
".jam" => "application/vnd.jam",
-
".jar" => "application/java-archive",
-
".java" => "text/x-java-source",
-
".jisp" => "application/vnd.jisp",
-
".jlt" => "application/vnd.hp-jlyt",
-
".jnlp" => "application/x-java-jnlp-file",
-
".joda" => "application/vnd.joost.joda-archive",
-
".jp2" => "image/jp2",
-
".jpeg" => "image/jpeg",
-
".jpg" => "image/jpeg",
-
".jpgv" => "video/jpeg",
-
".jpm" => "video/jpm",
-
".js" => "application/javascript",
-
".json" => "application/json",
-
".karbon" => "application/vnd.kde.karbon",
-
".kfo" => "application/vnd.kde.kformula",
-
".kia" => "application/vnd.kidspiration",
-
".kml" => "application/vnd.google-earth.kml+xml",
-
".kmz" => "application/vnd.google-earth.kmz",
-
".kne" => "application/vnd.kinar",
-
".kon" => "application/vnd.kde.kontour",
-
".kpr" => "application/vnd.kde.kpresenter",
-
".ksp" => "application/vnd.kde.kspread",
-
".ktz" => "application/vnd.kahootz",
-
".kwd" => "application/vnd.kde.kword",
-
".latex" => "application/x-latex",
-
".lbd" => "application/vnd.llamagraphics.life-balance.desktop",
-
".lbe" => "application/vnd.llamagraphics.life-balance.exchange+xml",
-
".les" => "application/vnd.hhe.lesson-player",
-
".link66" => "application/vnd.route66.link66+xml",
-
".log" => "text/plain",
-
".lostxml" => "application/lost+xml",
-
".lrm" => "application/vnd.ms-lrm",
-
".ltf" => "application/vnd.frogans.ltf",
-
".lvp" => "audio/vnd.lucent.voice",
-
".lwp" => "application/vnd.lotus-wordpro",
-
".m3u" => "audio/x-mpegurl",
-
".m4a" => "audio/mp4a-latm",
-
".m4v" => "video/mp4",
-
".ma" => "application/mathematica",
-
".mag" => "application/vnd.ecowin.chart",
-
".man" => "text/troff",
-
".manifest" => "text/cache-manifest",
-
".mathml" => "application/mathml+xml",
-
".mbk" => "application/vnd.mobius.mbk",
-
".mbox" => "application/mbox",
-
".mc1" => "application/vnd.medcalcdata",
-
".mcd" => "application/vnd.mcd",
-
".mdb" => "application/x-msaccess",
-
".mdi" => "image/vnd.ms-modi",
-
".mdoc" => "text/troff",
-
".me" => "text/troff",
-
".mfm" => "application/vnd.mfmp",
-
".mgz" => "application/vnd.proteus.magazine",
-
".mid" => "audio/midi",
-
".midi" => "audio/midi",
-
".mif" => "application/vnd.mif",
-
".mime" => "message/rfc822",
-
".mj2" => "video/mj2",
-
".mlp" => "application/vnd.dolby.mlp",
-
".mmd" => "application/vnd.chipnuts.karaoke-mmd",
-
".mmf" => "application/vnd.smaf",
-
".mml" => "application/mathml+xml",
-
".mmr" => "image/vnd.fujixerox.edmics-mmr",
-
".mng" => "video/x-mng",
-
".mny" => "application/x-msmoney",
-
".mov" => "video/quicktime",
-
".movie" => "video/x-sgi-movie",
-
".mp3" => "audio/mpeg",
-
".mp4" => "video/mp4",
-
".mp4a" => "audio/mp4",
-
".mp4s" => "application/mp4",
-
".mp4v" => "video/mp4",
-
".mpc" => "application/vnd.mophun.certificate",
-
".mpeg" => "video/mpeg",
-
".mpg" => "video/mpeg",
-
".mpga" => "audio/mpeg",
-
".mpkg" => "application/vnd.apple.installer+xml",
-
".mpm" => "application/vnd.blueice.multipass",
-
".mpn" => "application/vnd.mophun.application",
-
".mpp" => "application/vnd.ms-project",
-
".mpy" => "application/vnd.ibm.minipay",
-
".mqy" => "application/vnd.mobius.mqy",
-
".mrc" => "application/marc",
-
".ms" => "text/troff",
-
".mscml" => "application/mediaservercontrol+xml",
-
".mseq" => "application/vnd.mseq",
-
".msf" => "application/vnd.epson.msf",
-
".msh" => "model/mesh",
-
".msi" => "application/x-msdownload",
-
".msl" => "application/vnd.mobius.msl",
-
".msty" => "application/vnd.muvee.style",
-
".mts" => "model/vnd.mts",
-
".mus" => "application/vnd.musician",
-
".mvb" => "application/x-msmediaview",
-
".mwf" => "application/vnd.mfer",
-
".mxf" => "application/mxf",
-
".mxl" => "application/vnd.recordare.musicxml",
-
".mxml" => "application/xv+xml",
-
".mxs" => "application/vnd.triscape.mxs",
-
".mxu" => "video/vnd.mpegurl",
-
".n" => "application/vnd.nokia.n-gage.symbian.install",
-
".nc" => "application/x-netcdf",
-
".ngdat" => "application/vnd.nokia.n-gage.data",
-
".nlu" => "application/vnd.neurolanguage.nlu",
-
".nml" => "application/vnd.enliven",
-
".nnd" => "application/vnd.noblenet-directory",
-
".nns" => "application/vnd.noblenet-sealer",
-
".nnw" => "application/vnd.noblenet-web",
-
".npx" => "image/vnd.net-fpx",
-
".nsf" => "application/vnd.lotus-notes",
-
".oa2" => "application/vnd.fujitsu.oasys2",
-
".oa3" => "application/vnd.fujitsu.oasys3",
-
".oas" => "application/vnd.fujitsu.oasys",
-
".obd" => "application/x-msbinder",
-
".oda" => "application/oda",
-
".odc" => "application/vnd.oasis.opendocument.chart",
-
".odf" => "application/vnd.oasis.opendocument.formula",
-
".odg" => "application/vnd.oasis.opendocument.graphics",
-
".odi" => "application/vnd.oasis.opendocument.image",
-
".odp" => "application/vnd.oasis.opendocument.presentation",
-
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
-
".odt" => "application/vnd.oasis.opendocument.text",
-
".oga" => "audio/ogg",
-
".ogg" => "application/ogg",
-
".ogv" => "video/ogg",
-
".ogx" => "application/ogg",
-
".org" => "application/vnd.lotus-organizer",
-
".otc" => "application/vnd.oasis.opendocument.chart-template",
-
".otf" => "application/vnd.oasis.opendocument.formula-template",
-
".otg" => "application/vnd.oasis.opendocument.graphics-template",
-
".oth" => "application/vnd.oasis.opendocument.text-web",
-
".oti" => "application/vnd.oasis.opendocument.image-template",
-
".otm" => "application/vnd.oasis.opendocument.text-master",
-
".ots" => "application/vnd.oasis.opendocument.spreadsheet-template",
-
".ott" => "application/vnd.oasis.opendocument.text-template",
-
".oxt" => "application/vnd.openofficeorg.extension",
-
".p" => "text/x-pascal",
-
".p10" => "application/pkcs10",
-
".p12" => "application/x-pkcs12",
-
".p7b" => "application/x-pkcs7-certificates",
-
".p7m" => "application/pkcs7-mime",
-
".p7r" => "application/x-pkcs7-certreqresp",
-
".p7s" => "application/pkcs7-signature",
-
".pas" => "text/x-pascal",
-
".pbd" => "application/vnd.powerbuilder6",
-
".pbm" => "image/x-portable-bitmap",
-
".pcl" => "application/vnd.hp-pcl",
-
".pclxl" => "application/vnd.hp-pclxl",
-
".pcx" => "image/x-pcx",
-
".pdb" => "chemical/x-pdb",
-
".pdf" => "application/pdf",
-
".pem" => "application/x-x509-ca-cert",
-
".pfr" => "application/font-tdpfr",
-
".pgm" => "image/x-portable-graymap",
-
".pgn" => "application/x-chess-pgn",
-
".pgp" => "application/pgp-encrypted",
-
".pic" => "image/x-pict",
-
".pict" => "image/pict",
-
".pkg" => "application/octet-stream",
-
".pki" => "application/pkixcmp",
-
".pkipath" => "application/pkix-pkipath",
-
".pl" => "text/x-script.perl",
-
".plb" => "application/vnd.3gpp.pic-bw-large",
-
".plc" => "application/vnd.mobius.plc",
-
".plf" => "application/vnd.pocketlearn",
-
".pls" => "application/pls+xml",
-
".pm" => "text/x-script.perl-module",
-
".pml" => "application/vnd.ctc-posml",
-
".png" => "image/png",
-
".pnm" => "image/x-portable-anymap",
-
".pntg" => "image/x-macpaint",
-
".portpkg" => "application/vnd.macports.portpkg",
-
".ppd" => "application/vnd.cups-ppd",
-
".ppm" => "image/x-portable-pixmap",
-
".pps" => "application/vnd.ms-powerpoint",
-
".ppt" => "application/vnd.ms-powerpoint",
-
".prc" => "application/vnd.palm",
-
".pre" => "application/vnd.lotus-freelance",
-
".prf" => "application/pics-rules",
-
".ps" => "application/postscript",
-
".psb" => "application/vnd.3gpp.pic-bw-small",
-
".psd" => "image/vnd.adobe.photoshop",
-
".ptid" => "application/vnd.pvi.ptid1",
-
".pub" => "application/x-mspublisher",
-
".pvb" => "application/vnd.3gpp.pic-bw-var",
-
".pwn" => "application/vnd.3m.post-it-notes",
-
".py" => "text/x-script.python",
-
".pya" => "audio/vnd.ms-playready.media.pya",
-
".pyv" => "video/vnd.ms-playready.media.pyv",
-
".qam" => "application/vnd.epson.quickanime",
-
".qbo" => "application/vnd.intu.qbo",
-
".qfx" => "application/vnd.intu.qfx",
-
".qps" => "application/vnd.publishare-delta-tree",
-
".qt" => "video/quicktime",
-
".qtif" => "image/x-quicktime",
-
".qxd" => "application/vnd.quark.quarkxpress",
-
".ra" => "audio/x-pn-realaudio",
-
".rake" => "text/x-script.ruby",
-
".ram" => "audio/x-pn-realaudio",
-
".rar" => "application/x-rar-compressed",
-
".ras" => "image/x-cmu-raster",
-
".rb" => "text/x-script.ruby",
-
".rcprofile" => "application/vnd.ipunplugged.rcprofile",
-
".rdf" => "application/rdf+xml",
-
".rdz" => "application/vnd.data-vision.rdz",
-
".rep" => "application/vnd.businessobjects",
-
".rgb" => "image/x-rgb",
-
".rif" => "application/reginfo+xml",
-
".rl" => "application/resource-lists+xml",
-
".rlc" => "image/vnd.fujixerox.edmics-rlc",
-
".rld" => "application/resource-lists-diff+xml",
-
".rm" => "application/vnd.rn-realmedia",
-
".rmp" => "audio/x-pn-realaudio-plugin",
-
".rms" => "application/vnd.jcp.javame.midlet-rms",
-
".rnc" => "application/relax-ng-compact-syntax",
-
".roff" => "text/troff",
-
".rpm" => "application/x-redhat-package-manager",
-
".rpss" => "application/vnd.nokia.radio-presets",
-
".rpst" => "application/vnd.nokia.radio-preset",
-
".rq" => "application/sparql-query",
-
".rs" => "application/rls-services+xml",
-
".rsd" => "application/rsd+xml",
-
".rss" => "application/rss+xml",
-
".rtf" => "application/rtf",
-
".rtx" => "text/richtext",
-
".ru" => "text/x-script.ruby",
-
".s" => "text/x-asm",
-
".saf" => "application/vnd.yamaha.smaf-audio",
-
".sbml" => "application/sbml+xml",
-
".sc" => "application/vnd.ibm.secure-container",
-
".scd" => "application/x-msschedule",
-
".scm" => "application/vnd.lotus-screencam",
-
".scq" => "application/scvp-cv-request",
-
".scs" => "application/scvp-cv-response",
-
".sdkm" => "application/vnd.solent.sdkm+xml",
-
".sdp" => "application/sdp",
-
".see" => "application/vnd.seemail",
-
".sema" => "application/vnd.sema",
-
".semd" => "application/vnd.semd",
-
".semf" => "application/vnd.semf",
-
".setpay" => "application/set-payment-initiation",
-
".setreg" => "application/set-registration-initiation",
-
".sfd" => "application/vnd.hydrostatix.sof-data",
-
".sfs" => "application/vnd.spotfire.sfs",
-
".sgm" => "text/sgml",
-
".sgml" => "text/sgml",
-
".sh" => "application/x-sh",
-
".shar" => "application/x-shar",
-
".shf" => "application/shf+xml",
-
".sig" => "application/pgp-signature",
-
".sit" => "application/x-stuffit",
-
".sitx" => "application/x-stuffitx",
-
".skp" => "application/vnd.koan",
-
".slt" => "application/vnd.epson.salt",
-
".smi" => "application/smil+xml",
-
".snd" => "audio/basic",
-
".so" => "application/octet-stream",
-
".spf" => "application/vnd.yamaha.smaf-phrase",
-
".spl" => "application/x-futuresplash",
-
".spot" => "text/vnd.in3d.spot",
-
".spp" => "application/scvp-vp-response",
-
".spq" => "application/scvp-vp-request",
-
".src" => "application/x-wais-source",
-
".srx" => "application/sparql-results+xml",
-
".sse" => "application/vnd.kodak-descriptor",
-
".ssf" => "application/vnd.epson.ssf",
-
".ssml" => "application/ssml+xml",
-
".stf" => "application/vnd.wt.stf",
-
".stk" => "application/hyperstudio",
-
".str" => "application/vnd.pg.format",
-
".sus" => "application/vnd.sus-calendar",
-
".sv4cpio" => "application/x-sv4cpio",
-
".sv4crc" => "application/x-sv4crc",
-
".svd" => "application/vnd.svd",
-
".svg" => "image/svg+xml",
-
".svgz" => "image/svg+xml",
-
".swf" => "application/x-shockwave-flash",
-
".swi" => "application/vnd.arastra.swi",
-
".t" => "text/troff",
-
".tao" => "application/vnd.tao.intent-module-archive",
-
".tar" => "application/x-tar",
-
".tbz" => "application/x-bzip-compressed-tar",
-
".tcap" => "application/vnd.3gpp2.tcap",
-
".tcl" => "application/x-tcl",
-
".tex" => "application/x-tex",
-
".texi" => "application/x-texinfo",
-
".texinfo" => "application/x-texinfo",
-
".text" => "text/plain",
-
".tif" => "image/tiff",
-
".tiff" => "image/tiff",
-
".tmo" => "application/vnd.tmobile-livetv",
-
".torrent" => "application/x-bittorrent",
-
".tpl" => "application/vnd.groove-tool-template",
-
".tpt" => "application/vnd.trid.tpt",
-
".tr" => "text/troff",
-
".tra" => "application/vnd.trueapp",
-
".trm" => "application/x-msterminal",
-
".tsv" => "text/tab-separated-values",
-
".ttf" => "application/octet-stream",
-
".twd" => "application/vnd.simtech-mindmapper",
-
".txd" => "application/vnd.genomatix.tuxedo",
-
".txf" => "application/vnd.mobius.txf",
-
".txt" => "text/plain",
-
".ufd" => "application/vnd.ufdl",
-
".umj" => "application/vnd.umajin",
-
".unityweb" => "application/vnd.unity",
-
".uoml" => "application/vnd.uoml+xml",
-
".uri" => "text/uri-list",
-
".ustar" => "application/x-ustar",
-
".utz" => "application/vnd.uiq.theme",
-
".uu" => "text/x-uuencode",
-
".vcd" => "application/x-cdlink",
-
".vcf" => "text/x-vcard",
-
".vcg" => "application/vnd.groove-vcard",
-
".vcs" => "text/x-vcalendar",
-
".vcx" => "application/vnd.vcx",
-
".vis" => "application/vnd.visionary",
-
".viv" => "video/vnd.vivo",
-
".vrml" => "model/vrml",
-
".vsd" => "application/vnd.visio",
-
".vsf" => "application/vnd.vsf",
-
".vtu" => "model/vnd.vtu",
-
".vxml" => "application/voicexml+xml",
-
".war" => "application/java-archive",
-
".wav" => "audio/x-wav",
-
".wax" => "audio/x-ms-wax",
-
".wbmp" => "image/vnd.wap.wbmp",
-
".wbs" => "application/vnd.criticaltools.wbs+xml",
-
".wbxml" => "application/vnd.wap.wbxml",
-
".webm" => "video/webm",
-
".wm" => "video/x-ms-wm",
-
".wma" => "audio/x-ms-wma",
-
".wmd" => "application/x-ms-wmd",
-
".wmf" => "application/x-msmetafile",
-
".wml" => "text/vnd.wap.wml",
-
".wmlc" => "application/vnd.wap.wmlc",
-
".wmls" => "text/vnd.wap.wmlscript",
-
".wmlsc" => "application/vnd.wap.wmlscriptc",
-
".wmv" => "video/x-ms-wmv",
-
".wmx" => "video/x-ms-wmx",
-
".wmz" => "application/x-ms-wmz",
-
".woff" => "application/font-woff",
-
".woff2" => "application/font-woff2",
-
".wpd" => "application/vnd.wordperfect",
-
".wpl" => "application/vnd.ms-wpl",
-
".wps" => "application/vnd.ms-works",
-
".wqd" => "application/vnd.wqd",
-
".wri" => "application/x-mswrite",
-
".wrl" => "model/vrml",
-
".wsdl" => "application/wsdl+xml",
-
".wspolicy" => "application/wspolicy+xml",
-
".wtb" => "application/vnd.webturbo",
-
".wvx" => "video/x-ms-wvx",
-
".x3d" => "application/vnd.hzn-3d-crossword",
-
".xar" => "application/vnd.xara",
-
".xbd" => "application/vnd.fujixerox.docuworks.binder",
-
".xbm" => "image/x-xbitmap",
-
".xdm" => "application/vnd.syncml.dm+xml",
-
".xdp" => "application/vnd.adobe.xdp+xml",
-
".xdw" => "application/vnd.fujixerox.docuworks",
-
".xenc" => "application/xenc+xml",
-
".xer" => "application/patch-ops-error+xml",
-
".xfdf" => "application/vnd.adobe.xfdf",
-
".xfdl" => "application/vnd.xfdl",
-
".xhtml" => "application/xhtml+xml",
-
".xif" => "image/vnd.xiff",
-
".xls" => "application/vnd.ms-excel",
-
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
-
".xml" => "application/xml",
-
".xo" => "application/vnd.olpc-sugar",
-
".xop" => "application/xop+xml",
-
".xpm" => "image/x-xpixmap",
-
".xpr" => "application/vnd.is-xpr",
-
".xps" => "application/vnd.ms-xpsdocument",
-
".xpw" => "application/vnd.intercon.formnet",
-
".xsl" => "application/xml",
-
".xslt" => "application/xslt+xml",
-
".xsm" => "application/vnd.syncml+xml",
-
".xspf" => "application/xspf+xml",
-
".xul" => "application/vnd.mozilla.xul+xml",
-
".xwd" => "image/x-xwindowdump",
-
".xyz" => "chemical/x-xyz",
-
".yaml" => "text/yaml",
-
".yml" => "text/yaml",
-
".zaz" => "application/vnd.zzazz.deck+xml",
-
".zip" => "application/zip",
-
".zmm" => "application/vnd.handheld-entertainment+xml",
-
}
-
end
-
end
-
2
require 'uri'
-
2
require 'stringio'
-
2
require 'rack'
-
2
require 'rack/lint'
-
2
require 'rack/utils'
-
2
require 'rack/response'
-
-
2
module Rack
-
# Rack::MockRequest helps testing your Rack application without
-
# actually using HTTP.
-
#
-
# After performing a request on a URL with get/post/put/patch/delete, it
-
# returns a MockResponse with useful helper methods for effective
-
# testing.
-
#
-
# You can pass a hash with additional configuration to the
-
# get/post/put/patch/delete.
-
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
-
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
-
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
-
-
2
class MockRequest
-
2
class FatalWarning < RuntimeError
-
end
-
-
2
class FatalWarner
-
2
def puts(warning)
-
raise FatalWarning, warning
-
end
-
-
2
def write(warning)
-
raise FatalWarning, warning
-
end
-
-
2
def flush
-
end
-
-
2
def string
-
""
-
end
-
end
-
-
2
DEFAULT_ENV = {
-
"rack.version" => Rack::VERSION,
-
"rack.input" => StringIO.new,
-
"rack.errors" => StringIO.new,
-
"rack.multithread" => true,
-
"rack.multiprocess" => true,
-
"rack.run_once" => false,
-
}
-
-
2
def initialize(app)
-
@app = app
-
end
-
-
2
def get(uri, opts={}) request("GET", uri, opts) end
-
2
def post(uri, opts={}) request("POST", uri, opts) end
-
2
def put(uri, opts={}) request("PUT", uri, opts) end
-
2
def patch(uri, opts={}) request("PATCH", uri, opts) end
-
2
def delete(uri, opts={}) request("DELETE", uri, opts) end
-
2
def head(uri, opts={}) request("HEAD", uri, opts) end
-
2
def options(uri, opts={}) request("OPTIONS", uri, opts) end
-
-
2
def request(method="GET", uri="", opts={})
-
env = self.class.env_for(uri, opts.merge(:method => method))
-
-
if opts[:lint]
-
app = Rack::Lint.new(@app)
-
else
-
app = @app
-
end
-
-
errors = env["rack.errors"]
-
status, headers, body = app.call(env)
-
MockResponse.new(status, headers, body, errors)
-
ensure
-
body.close if body.respond_to?(:close)
-
end
-
-
# For historical reasons, we're pinning to RFC 2396. It's easier for users
-
# and we get support from ruby 1.8 to 2.2 using this method.
-
2
def self.parse_uri_rfc2396(uri)
-
2
@parser ||= defined?(URI::RFC2396_Parser) ? URI::RFC2396_Parser.new : URI
-
2
@parser.parse(uri)
-
end
-
-
# Return the Rack environment used for a request to +uri+.
-
2
def self.env_for(uri="", opts={})
-
2
uri = parse_uri_rfc2396(uri)
-
2
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
-
-
2
env = DEFAULT_ENV.dup
-
-
2
env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : "GET"
-
2
env["SERVER_NAME"] = uri.host || "example.org"
-
2
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
-
2
env[QUERY_STRING] = uri.query.to_s
-
2
env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path
-
2
env["rack.url_scheme"] = uri.scheme || "http"
-
2
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
-
-
2
env[SCRIPT_NAME] = opts[:script_name] || ""
-
-
2
if opts[:fatal]
-
env["rack.errors"] = FatalWarner.new
-
else
-
2
env["rack.errors"] = StringIO.new
-
end
-
-
2
if params = opts[:params]
-
if env[REQUEST_METHOD] == "GET"
-
params = Utils.parse_nested_query(params) if params.is_a?(String)
-
params.update(Utils.parse_nested_query(env[QUERY_STRING]))
-
env[QUERY_STRING] = Utils.build_nested_query(params)
-
elsif !opts.has_key?(:input)
-
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
-
if params.is_a?(Hash)
-
if data = Utils::Multipart.build_multipart(params)
-
opts[:input] = data
-
opts["CONTENT_LENGTH"] ||= data.length.to_s
-
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
-
else
-
opts[:input] = Utils.build_nested_query(params)
-
end
-
else
-
opts[:input] = params
-
end
-
end
-
end
-
-
2
empty_str = ""
-
2
empty_str.force_encoding("ASCII-8BIT") if empty_str.respond_to? :force_encoding
-
2
opts[:input] ||= empty_str
-
2
if String === opts[:input]
-
2
rack_input = StringIO.new(opts[:input])
-
else
-
rack_input = opts[:input]
-
end
-
-
2
rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
-
2
env['rack.input'] = rack_input
-
-
2
env["CONTENT_LENGTH"] ||= env["rack.input"].length.to_s
-
-
2
opts.each { |field, value|
-
8
env[field] = value if String === field
-
}
-
-
2
env
-
end
-
end
-
-
# Rack::MockResponse provides useful helpers for testing your apps.
-
# Usually, you don't create the MockResponse on your own, but use
-
# MockRequest.
-
-
2
class MockResponse < Rack::Response
-
# Headers
-
2
attr_reader :original_headers
-
-
# Errors
-
2
attr_accessor :errors
-
-
2
def initialize(status, headers, body, errors=StringIO.new(""))
-
@original_headers = headers
-
@errors = errors.string if errors.respond_to?(:string)
-
@body_string = nil
-
-
super(body, status, headers)
-
end
-
-
2
def =~(other)
-
body =~ other
-
end
-
-
2
def match(other)
-
body.match other
-
end
-
-
2
def body
-
# FIXME: apparently users of MockResponse expect the return value of
-
# MockResponse#body to be a string. However, the real response object
-
# returns the body as a list.
-
#
-
# See spec_showstatus.rb:
-
#
-
# should "not replace existing messages" do
-
# ...
-
# res.body.should == "foo!"
-
# end
-
super.join
-
end
-
-
2
def empty?
-
[201, 204, 205, 304].include? status
-
end
-
end
-
end
-
2
module Rack
-
# Sets an "X-Runtime" response header, indicating the response
-
# time of the request, in seconds
-
#
-
# You can put it right before the application to see the processing
-
# time, or before all the other middlewares to include time for them,
-
# too.
-
2
class Runtime
-
2
def initialize(app, name = nil)
-
2
@app = app
-
2
@header_name = "X-Runtime"
-
2
@header_name << "-#{name}" if name
-
end
-
-
2
FORMAT_STRING = "%0.6f"
-
2
def call(env)
-
start_time = clock_time
-
status, headers, body = @app.call(env)
-
request_time = clock_time - start_time
-
-
if !headers.has_key?(@header_name)
-
headers[@header_name] = FORMAT_STRING % request_time
-
end
-
-
[status, headers, body]
-
end
-
-
2
private
-
-
2
if defined?(Process::CLOCK_MONOTONIC)
-
2
def clock_time
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
end
-
else
-
def clock_time
-
Time.now.to_f
-
end
-
end
-
end
-
end
-
2
require 'rack/file'
-
2
require 'rack/body_proxy'
-
-
2
module Rack
-
-
# = Sendfile
-
#
-
# The Sendfile middleware intercepts responses whose body is being
-
# served from a file and replaces it with a server specific X-Sendfile
-
# header. The web server is then responsible for writing the file contents
-
# to the client. This can dramatically reduce the amount of work required
-
# by the Ruby backend and takes advantage of the web server's optimized file
-
# delivery code.
-
#
-
# In order to take advantage of this middleware, the response body must
-
# respond to +to_path+ and the request must include an X-Sendfile-Type
-
# header. Rack::File and other components implement +to_path+ so there's
-
# rarely anything you need to do in your application. The X-Sendfile-Type
-
# header is typically set in your web servers configuration. The following
-
# sections attempt to document
-
#
-
# === Nginx
-
#
-
# Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile
-
# but requires parts of the filesystem to be mapped into a private URL
-
# hierarchy.
-
#
-
# The following example shows the Nginx configuration required to create
-
# a private "/files/" area, enable X-Accel-Redirect, and pass the special
-
# X-Sendfile-Type and X-Accel-Mapping headers to the backend:
-
#
-
# location ~ /files/(.*) {
-
# internal;
-
# alias /var/www/$1;
-
# }
-
#
-
# location / {
-
# proxy_redirect off;
-
#
-
# proxy_set_header Host $host;
-
# proxy_set_header X-Real-IP $remote_addr;
-
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-
#
-
# proxy_set_header X-Sendfile-Type X-Accel-Redirect;
-
# proxy_set_header X-Accel-Mapping /var/www/=/files/;
-
#
-
# proxy_pass http://127.0.0.1:8080/;
-
# }
-
#
-
# Note that the X-Sendfile-Type header must be set exactly as shown above.
-
# The X-Accel-Mapping header should specify the location on the file system,
-
# followed by an equals sign (=), followed name of the private URL pattern
-
# that it maps to. The middleware performs a simple substitution on the
-
# resulting path.
-
#
-
# See Also: http://wiki.codemongers.com/NginxXSendfile
-
#
-
# === lighttpd
-
#
-
# Lighttpd has supported some variation of the X-Sendfile header for some
-
# time, although only recent version support X-Sendfile in a reverse proxy
-
# configuration.
-
#
-
# $HTTP["host"] == "example.com" {
-
# proxy-core.protocol = "http"
-
# proxy-core.balancer = "round-robin"
-
# proxy-core.backends = (
-
# "127.0.0.1:8000",
-
# "127.0.0.1:8001",
-
# ...
-
# )
-
#
-
# proxy-core.allow-x-sendfile = "enable"
-
# proxy-core.rewrite-request = (
-
# "X-Sendfile-Type" => (".*" => "X-Sendfile")
-
# )
-
# }
-
#
-
# See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
-
#
-
# === Apache
-
#
-
# X-Sendfile is supported under Apache 2.x using a separate module:
-
#
-
# https://tn123.org/mod_xsendfile/
-
#
-
# Once the module is compiled and installed, you can enable it using
-
# XSendFile config directive:
-
#
-
# RequestHeader Set X-Sendfile-Type X-Sendfile
-
# ProxyPassReverse / http://localhost:8001/
-
# XSendFile on
-
#
-
# === Mapping parameter
-
#
-
# The third parameter allows for an overriding extension of the
-
# X-Accel-Mapping header. Mappings should be provided in tuples of internal to
-
# external. The internal values may contain regular expression syntax, they
-
# will be matched with case indifference.
-
-
2
class Sendfile
-
2
F = ::File
-
-
2
def initialize(app, variation=nil, mappings=[])
-
2
@app = app
-
2
@variation = variation
-
2
@mappings = mappings.map do |internal, external|
-
[/^#{internal}/i, external]
-
end
-
end
-
-
2
def call(env)
-
status, headers, body = @app.call(env)
-
if body.respond_to?(:to_path)
-
case type = variation(env)
-
when 'X-Accel-Redirect'
-
path = F.expand_path(body.to_path)
-
if url = map_accel_path(env, path)
-
headers[CONTENT_LENGTH] = '0'
-
headers[type] = url
-
obody = body
-
body = Rack::BodyProxy.new([]) do
-
obody.close if obody.respond_to?(:close)
-
end
-
else
-
env['rack.errors'].puts "X-Accel-Mapping header missing"
-
end
-
when 'X-Sendfile', 'X-Lighttpd-Send-File'
-
path = F.expand_path(body.to_path)
-
headers[CONTENT_LENGTH] = '0'
-
headers[type] = path
-
obody = body
-
body = Rack::BodyProxy.new([]) do
-
obody.close if obody.respond_to?(:close)
-
end
-
when '', nil
-
else
-
env['rack.errors'].puts "Unknown x-sendfile variation: '#{type}'.\n"
-
end
-
end
-
[status, headers, body]
-
end
-
-
2
private
-
2
def variation(env)
-
@variation ||
-
env['sendfile.type'] ||
-
env['HTTP_X_SENDFILE_TYPE']
-
end
-
-
2
def map_accel_path(env, path)
-
if mapping = @mappings.find { |internal,_| internal =~ path }
-
path.sub(*mapping)
-
elsif mapping = env['HTTP_X_ACCEL_MAPPING']
-
internal, external = mapping.split('=', 2).map{ |p| p.strip }
-
path.sub(/^#{internal}/i, external)
-
end
-
end
-
end
-
end
-
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
-
# bugrep: Andreas Zehnder
-
-
2
require 'time'
-
2
require 'rack/request'
-
2
require 'rack/response'
-
2
begin
-
2
require 'securerandom'
-
rescue LoadError
-
# We just won't get securerandom
-
end
-
-
2
module Rack
-
-
2
module Session
-
-
2
module Abstract
-
2
ENV_SESSION_KEY = 'rack.session'.freeze
-
2
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
-
-
# SessionHash is responsible to lazily load the session from store.
-
-
2
class SessionHash
-
2
include Enumerable
-
2
attr_writer :id
-
-
2
def self.find(env)
-
env[ENV_SESSION_KEY]
-
end
-
-
2
def self.set(env, session)
-
env[ENV_SESSION_KEY] = session
-
end
-
-
2
def self.set_options(env, options)
-
env[ENV_SESSION_OPTIONS_KEY] = options.dup
-
end
-
-
2
def initialize(store, env)
-
29
@store = store
-
29
@env = env
-
29
@loaded = false
-
end
-
-
2
def id
-
return @id if @loaded or instance_variable_defined?(:@id)
-
@id = @store.send(:extract_session_id, @env)
-
end
-
-
2
def options
-
@env[ENV_SESSION_OPTIONS_KEY]
-
end
-
-
2
def each(&block)
-
load_for_read!
-
@data.each(&block)
-
end
-
-
2
def [](key)
-
85
load_for_read!
-
85
@data[key.to_s]
-
end
-
2
alias :fetch :[]
-
-
2
def has_key?(key)
-
load_for_read!
-
@data.has_key?(key.to_s)
-
end
-
2
alias :key? :has_key?
-
2
alias :include? :has_key?
-
-
2
def []=(key, value)
-
67
load_for_write!
-
67
@data[key.to_s] = value
-
end
-
2
alias :store :[]=
-
-
2
def clear
-
load_for_write!
-
@data.clear
-
end
-
-
2
def destroy
-
clear
-
@id = @store.send(:destroy_session, @env, id, options)
-
end
-
-
2
def to_hash
-
load_for_read!
-
@data.dup
-
end
-
-
2
def update(hash)
-
load_for_write!
-
@data.update(stringify_keys(hash))
-
end
-
2
alias :merge! :update
-
-
2
def replace(hash)
-
load_for_write!
-
@data.replace(stringify_keys(hash))
-
end
-
-
2
def delete(key)
-
load_for_write!
-
@data.delete(key.to_s)
-
end
-
-
2
def inspect
-
if loaded?
-
@data.inspect
-
else
-
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
-
end
-
end
-
-
2
def exists?
-
return @exists if instance_variable_defined?(:@exists)
-
@data = {}
-
@exists = @store.send(:session_exists?, @env)
-
end
-
-
2
def loaded?
-
152
@loaded
-
end
-
-
2
def empty?
-
load_for_read!
-
@data.empty?
-
end
-
-
2
def keys
-
@data.keys
-
end
-
-
2
def values
-
@data.values
-
end
-
-
2
private
-
-
2
def load_for_read!
-
85
load! if !loaded? && exists?
-
end
-
-
2
def load_for_write!
-
67
load! unless loaded?
-
end
-
-
2
def load!
-
@id, session = @store.send(:load_session, @env)
-
@data = stringify_keys(session)
-
@loaded = true
-
end
-
-
2
def stringify_keys(other)
-
29
hash = {}
-
29
other.each do |key, value|
-
hash[key.to_s] = value
-
end
-
29
hash
-
end
-
end
-
-
# ID sets up a basic framework for implementing an id based sessioning
-
# service. Cookies sent to the client for maintaining sessions will only
-
# contain an id reference. Only #get_session and #set_session are
-
# required to be overwritten.
-
#
-
# All parameters are optional.
-
# * :key determines the name of the cookie, by default it is
-
# 'rack.session'
-
# * :path, :domain, :expire_after, :secure, and :httponly set the related
-
# cookie options as by Rack::Response#add_cookie
-
# * :skip will not a set a cookie in the response nor update the session state
-
# * :defer will not set a cookie in the response but still update the session
-
# state if it is used with a backend
-
# * :renew (implementation dependent) will prompt the generation of a new
-
# session id, and migration of data to be referenced at the new id. If
-
# :defer is set, it will be overridden and the cookie will be set.
-
# * :sidbits sets the number of bits in length that a generated session
-
# id will be.
-
#
-
# These options can be set on a per request basis, at the location of
-
# env['rack.session.options']. Additionally the id of the session can be
-
# found within the options hash at the key :id. It is highly not
-
# recommended to change its value.
-
#
-
# Is Rack::Utils::Context compatible.
-
#
-
# Not included by default; you must require 'rack/session/abstract/id'
-
# to use.
-
-
2
class ID
-
2
DEFAULT_OPTIONS = {
-
:key => 'rack.session',
-
:path => '/',
-
:domain => nil,
-
:expire_after => nil,
-
:secure => false,
-
:httponly => true,
-
:defer => false,
-
:renew => false,
-
:sidbits => 128,
-
:cookie_only => true,
-
2
:secure_random => (::SecureRandom rescue false)
-
}
-
-
2
attr_reader :key, :default_options
-
-
2
def initialize(app, options={})
-
2
@app = app
-
2
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
-
2
@key = @default_options.delete(:key)
-
2
@cookie_only = @default_options.delete(:cookie_only)
-
2
initialize_sid
-
end
-
-
2
def call(env)
-
context(env)
-
end
-
-
2
def context(env, app=@app)
-
prepare_session(env)
-
status, headers, body = app.call(env)
-
commit_session(env, status, headers, body)
-
end
-
-
2
private
-
-
2
def initialize_sid
-
@sidbits = @default_options[:sidbits]
-
@sid_secure = @default_options[:secure_random]
-
@sid_length = @sidbits / 4
-
end
-
-
# Generate a new session id using Ruby #rand. The size of the
-
# session id is controlled by the :sidbits option.
-
# Monkey patch this to use custom methods for session id generation.
-
-
2
def generate_sid(secure = @sid_secure)
-
if secure
-
secure.hex(@sid_length)
-
else
-
"%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
-
end
-
rescue NotImplementedError
-
generate_sid(false)
-
end
-
-
# Sets the lazy session at 'rack.session' and places options and session
-
# metadata into 'rack.session.options'.
-
-
2
def prepare_session(env)
-
session_was = env[ENV_SESSION_KEY]
-
env[ENV_SESSION_KEY] = session_class.new(self, env)
-
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
-
env[ENV_SESSION_KEY].merge! session_was if session_was
-
end
-
-
# Extracts the session id from provided cookies and passes it and the
-
# environment to #get_session.
-
-
2
def load_session(env)
-
sid = current_session_id(env)
-
sid, session = get_session(env, sid)
-
[sid, session || {}]
-
end
-
-
# Extract session id from request object.
-
-
2
def extract_session_id(env)
-
request = Rack::Request.new(env)
-
sid = request.cookies[@key]
-
sid ||= request.params[@key] unless @cookie_only
-
sid
-
end
-
-
# Returns the current session id from the SessionHash.
-
-
2
def current_session_id(env)
-
env[ENV_SESSION_KEY].id
-
end
-
-
# Check if the session exists or not.
-
-
2
def session_exists?(env)
-
value = current_session_id(env)
-
value && !value.empty?
-
end
-
-
# Session should be committed if it was loaded, any of specific options like :renew, :drop
-
# or :expire_after was given and the security permissions match. Skips if skip is given.
-
-
2
def commit_session?(env, session, options)
-
if options[:skip]
-
false
-
else
-
has_session = loaded_session?(session) || forced_session_update?(session, options)
-
has_session && security_matches?(env, options)
-
end
-
end
-
-
2
def loaded_session?(session)
-
!session.is_a?(session_class) || session.loaded?
-
end
-
-
2
def forced_session_update?(session, options)
-
force_options?(options) && session && !session.empty?
-
end
-
-
2
def force_options?(options)
-
options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
-
end
-
-
2
def security_matches?(env, options)
-
return true unless options[:secure]
-
request = Rack::Request.new(env)
-
request.ssl?
-
end
-
-
# Acquires the session from the environment and the session id from
-
# the session options and passes them to #set_session. If successful
-
# and the :defer option is not true, a cookie will be added to the
-
# response with the session's id.
-
-
2
def commit_session(env, status, headers, body)
-
session = env[ENV_SESSION_KEY]
-
options = session.options
-
-
if options[:drop] || options[:renew]
-
session_id = destroy_session(env, session.id || generate_sid, options)
-
return [status, headers, body] unless session_id
-
end
-
-
return [status, headers, body] unless commit_session?(env, session, options)
-
-
session.send(:load!) unless loaded_session?(session)
-
session_id ||= session.id
-
session_data = session.to_hash.delete_if { |k,v| v.nil? }
-
-
if not data = set_session(env, session_id, session_data, options)
-
env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
-
elsif options[:defer] and not options[:renew]
-
env["rack.errors"].puts("Deferring cookie for #{session_id}") if $VERBOSE
-
else
-
cookie = Hash.new
-
cookie[:value] = data
-
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
-
cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
-
set_cookie(env, headers, cookie.merge!(options))
-
end
-
-
[status, headers, body]
-
end
-
-
# Sets the cookie back to the client with session id. We skip the cookie
-
# setting if the value didn't change (sid is the same) or expires was given.
-
-
2
def set_cookie(env, headers, cookie)
-
request = Rack::Request.new(env)
-
if request.cookies[@key] != cookie[:value] || cookie[:expires]
-
Utils.set_cookie_header!(headers, @key, cookie)
-
end
-
end
-
-
# Allow subclasses to prepare_session for different Session classes
-
-
2
def session_class
-
SessionHash
-
end
-
-
# All thread safety and session retrieval procedures should occur here.
-
# Should return [session_id, session].
-
# If nil is provided as the session id, generation of a new valid id
-
# should occur within.
-
-
2
def get_session(env, sid)
-
raise '#get_session not implemented.'
-
end
-
-
# All thread safety and session storage procedures should occur here.
-
# Must return the session id if the session was saved successfully, or
-
# false if the session could not be saved.
-
-
2
def set_session(env, sid, session, options)
-
raise '#set_session not implemented.'
-
end
-
-
# All thread safety and session destroy procedures should occur here.
-
# Should return a new session id or nil if options[:drop]
-
-
2
def destroy_session(env, sid, options)
-
raise '#destroy_session not implemented'
-
end
-
end
-
end
-
end
-
end
-
2
require 'openssl'
-
2
require 'zlib'
-
2
require 'rack/request'
-
2
require 'rack/response'
-
2
require 'rack/session/abstract/id'
-
-
2
module Rack
-
-
2
module Session
-
-
# Rack::Session::Cookie provides simple cookie based session management.
-
# By default, the session is a Ruby Hash stored as base64 encoded marshalled
-
# data set to :key (default: rack.session). The object that encodes the
-
# session data is configurable and must respond to +encode+ and +decode+.
-
# Both methods must take a string and return a string.
-
#
-
# When the secret key is set, cookie data is checked for data integrity.
-
# The old secret key is also accepted and allows graceful secret rotation.
-
#
-
# Example:
-
#
-
# use Rack::Session::Cookie, :key => 'rack.session',
-
# :domain => 'foo.com',
-
# :path => '/',
-
# :expire_after => 2592000,
-
# :secret => 'change_me',
-
# :old_secret => 'also_change_me'
-
#
-
# All parameters are optional.
-
#
-
# Example of a cookie with no encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Rack::Session::Cookie::Identity.new
-
# })
-
#
-
# Example of a cookie with custom encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Class.new {
-
# def encode(str); str.reverse; end
-
# def decode(str); str.reverse; end
-
# }.new
-
# })
-
#
-
-
2
class Cookie < Abstract::ID
-
# Encode session cookies as Base64
-
2
class Base64
-
2
def encode(str)
-
[str].pack('m')
-
end
-
-
2
def decode(str)
-
str.unpack('m').first
-
end
-
-
# Encode session cookies as Marshaled Base64 data
-
2
class Marshal < Base64
-
2
def encode(str)
-
super(::Marshal.dump(str))
-
end
-
-
2
def decode(str)
-
return unless str
-
::Marshal.load(super(str)) rescue nil
-
end
-
end
-
-
# N.B. Unlike other encoding methods, the contained objects must be a
-
# valid JSON composite type, either a Hash or an Array.
-
2
class JSON < Base64
-
2
def encode(obj)
-
super(::Rack::Utils::OkJson.encode(obj))
-
end
-
-
2
def decode(str)
-
return unless str
-
::Rack::Utils::OkJson.decode(super(str)) rescue nil
-
end
-
end
-
-
2
class ZipJSON < Base64
-
2
def encode(obj)
-
super(Zlib::Deflate.deflate(::Rack::Utils::OkJson.encode(obj)))
-
end
-
-
2
def decode(str)
-
return unless str
-
::Rack::Utils::OkJson.decode(Zlib::Inflate.inflate(super(str)))
-
rescue
-
nil
-
end
-
end
-
end
-
-
# Use no encoding for session cookies
-
2
class Identity
-
2
def encode(str); str; end
-
2
def decode(str); str; end
-
end
-
-
2
attr_reader :coder
-
-
2
def initialize(app, options={})
-
@secrets = options.values_at(:secret, :old_secret).compact
-
warn <<-MSG unless @secrets.size >= 1
-
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
-
This poses a security threat. It is strongly recommended that you
-
provide a secret to prevent exploits that may be possible from crafted
-
cookies. This will not be supported in future versions of Rack, and
-
future versions will even invalidate your existing user cookies.
-
-
Called from: #{caller[0]}.
-
MSG
-
@coder = options[:coder] ||= Base64::Marshal.new
-
super(app, options.merge!(:cookie_only => true))
-
end
-
-
2
private
-
-
2
def get_session(env, sid)
-
data = unpacked_cookie_data(env)
-
data = persistent_session_id!(data)
-
[data["session_id"], data]
-
end
-
-
2
def extract_session_id(env)
-
unpacked_cookie_data(env)["session_id"]
-
end
-
-
2
def unpacked_cookie_data(env)
-
env["rack.session.unpacked_cookie_data"] ||= begin
-
request = Rack::Request.new(env)
-
session_data = request.cookies[@key]
-
-
if @secrets.size > 0 && session_data
-
digest, session_data = session_data.reverse.split("--", 2)
-
digest.reverse! if digest
-
session_data.reverse! if session_data
-
session_data = nil unless digest_match?(session_data, digest)
-
end
-
-
coder.decode(session_data) || {}
-
end
-
end
-
-
2
def persistent_session_id!(data, sid=nil)
-
data ||= {}
-
data["session_id"] ||= sid || generate_sid
-
data
-
end
-
-
2
def set_session(env, session_id, session, options)
-
session = session.merge("session_id" => session_id)
-
session_data = coder.encode(session)
-
-
if @secrets.first
-
session_data << "--#{generate_hmac(session_data, @secrets.first)}"
-
end
-
-
if session_data.size > (4096 - @key.size)
-
env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
-
nil
-
else
-
session_data
-
end
-
end
-
-
2
def destroy_session(env, session_id, options)
-
# Nothing to do here, data is in the client
-
generate_sid unless options[:drop]
-
end
-
-
2
def digest_match?(data, digest)
-
return unless data && digest
-
@secrets.any? do |secret|
-
Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
-
end
-
end
-
-
2
def generate_hmac(data, secret)
-
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
-
end
-
-
end
-
end
-
end
-
2
require 'active_support/dependencies/autoload'
-
2
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner"
-
-
2
module HTML
-
2
extend ActiveSupport::Autoload
-
-
2
eager_autoload do
-
2
autoload :CDATA, 'html/node'
-
2
autoload :Document, 'html/document'
-
2
autoload :FullSanitizer, 'html/sanitizer'
-
2
autoload :LinkSanitizer, 'html/sanitizer'
-
2
autoload :Node, 'html/node'
-
2
autoload :Sanitizer, 'html/sanitizer'
-
2
autoload :Selector, 'html/selector'
-
2
autoload :Tag, 'html/node'
-
2
autoload :Text, 'html/node'
-
2
autoload :Tokenizer, 'html/tokenizer'
-
2
autoload :Version, 'html/version'
-
2
autoload :WhiteListSanitizer, 'html/sanitizer'
-
end
-
end
-
2
require 'rails/dom/testing/assertions'
-
2
require 'active_support/concern'
-
2
require 'nokogiri'
-
-
2
module Rails
-
2
module Dom
-
2
module Testing
-
2
module Assertions
-
2
autoload :DomAssertions, 'rails/dom/testing/assertions/dom_assertions'
-
2
autoload :SelectorAssertions, 'rails/dom/testing/assertions/selector_assertions'
-
2
autoload :TagAssertions, 'rails/dom/testing/assertions/tag_assertions'
-
-
2
extend ActiveSupport::Concern
-
-
2
include DomAssertions
-
2
include SelectorAssertions
-
2
include TagAssertions
-
end
-
end
-
end
-
end
-
2
module Rails
-
2
module Dom
-
2
module Testing
-
2
module Assertions
-
2
module DomAssertions
-
# \Test two HTML strings for equivalency (e.g., equal even when attributes are in another order)
-
#
-
# # assert that the referenced method generates the appropriate HTML string
-
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
-
2
def assert_dom_equal(expected, actual, message = nil)
-
expected_dom, actual_dom = fragment(expected), fragment(actual)
-
message ||= "Expected: #{expected}\nActual: #{actual}"
-
assert compare_doms(expected_dom, actual_dom), message
-
end
-
-
# The negated form of +assert_dom_equal+.
-
#
-
# # assert that the referenced method does not generate the specified HTML string
-
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
-
2
def assert_dom_not_equal(expected, actual, message = nil)
-
expected_dom, actual_dom = fragment(expected), fragment(actual)
-
message ||= "Expected: #{expected}\nActual: #{actual}"
-
assert_not compare_doms(expected_dom, actual_dom), message
-
end
-
-
2
protected
-
-
2
def compare_doms(expected, actual)
-
return false unless expected.children.size == actual.children.size
-
-
expected.children.each_with_index do |child, i|
-
return false unless equal_children?(child, actual.children[i])
-
end
-
-
true
-
end
-
-
2
def equal_children?(child, other_child)
-
return false unless child.type == other_child.type
-
-
if child.element?
-
child.name == other_child.name &&
-
equal_attribute_nodes?(child.attribute_nodes, other_child.attribute_nodes) &&
-
compare_doms(child, other_child)
-
else
-
child.to_s == other_child.to_s
-
end
-
end
-
-
2
def equal_attribute_nodes?(nodes, other_nodes)
-
return false unless nodes.size == other_nodes.size
-
-
nodes = nodes.sort_by(&:name)
-
other_nodes = other_nodes.sort_by(&:name)
-
-
nodes.each_with_index do |attr, i|
-
return false unless equal_attribute?(attr, other_nodes[i])
-
end
-
-
true
-
end
-
-
2
def equal_attribute?(attr, other_attr)
-
attr.name == other_attr.name && attr.value == other_attr.value
-
end
-
-
2
private
-
-
2
def fragment(text)
-
Nokogiri::HTML::DocumentFragment.parse(text)
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/deprecation'
-
2
require 'rails/deprecated_sanitizer/html-scanner'
-
-
2
module Rails
-
2
module Dom
-
2
module Testing
-
2
module Assertions
-
# Pair of assertions to testing elements in the HTML output of the response.
-
2
module TagAssertions
-
# Asserts that there is a tag/node/element in the body of the response
-
# that meets all of the given conditions. The +conditions+ parameter must
-
# be a hash of any of the following keys (all are optional):
-
#
-
# * <tt>:tag</tt>: the node type must match the corresponding value
-
# * <tt>:attributes</tt>: a hash. The node's attributes must match the
-
# corresponding values in the hash.
-
# * <tt>:parent</tt>: a hash. The node's parent must match the
-
# corresponding hash.
-
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
-
# must meet the criteria described by the hash.
-
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
-
# meet the criteria described by the hash.
-
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
-
# must meet the criteria described by the hash.
-
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
-
# meet the criteria described by the hash.
-
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
-
# the criteria described by the hash, and at least one sibling must match.
-
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
-
# the criteria described by the hash, and at least one sibling must match.
-
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts
-
# the keys:
-
# * <tt>:count</tt>: either a number or a range which must equal (or
-
# include) the number of children that match.
-
# * <tt>:less_than</tt>: the number of matching children must be less
-
# than this number.
-
# * <tt>:greater_than</tt>: the number of matching children must be
-
# greater than this number.
-
# * <tt>:only</tt>: another hash consisting of the keys to use
-
# to match on the children, and only matching children will be
-
# counted.
-
# * <tt>:content</tt>: the textual content of the node must match the
-
# given value. This will not match HTML tags in the body of a
-
# tag--only text.
-
#
-
# Conditions are matched using the following algorithm:
-
#
-
# * if the condition is a string, it must be a substring of the value.
-
# * if the condition is a regexp, it must match the value.
-
# * if the condition is a number, the value must match number.to_s.
-
# * if the condition is +true+, the value must not be +nil+.
-
# * if the condition is +false+ or +nil+, the value must be +nil+.
-
#
-
# # Assert that there is a "span" tag
-
# assert_tag tag: "span"
-
#
-
# # Assert that there is a "span" tag with id="x"
-
# assert_tag tag: "span", attributes: { id: "x" }
-
#
-
# # Assert that there is a "span" tag using the short-hand
-
# assert_tag :span
-
#
-
# # Assert that there is a "span" tag with id="x" using the short-hand
-
# assert_tag :span, attributes: { id: "x" }
-
#
-
# # Assert that there is a "span" inside of a "div"
-
# assert_tag tag: "span", parent: { tag: "div" }
-
#
-
# # Assert that there is a "span" somewhere inside a table
-
# assert_tag tag: "span", ancestor: { tag: "table" }
-
#
-
# # Assert that there is a "span" with at least one "em" child
-
# assert_tag tag: "span", child: { tag: "em" }
-
#
-
# # Assert that there is a "span" containing a (possibly nested)
-
# # "strong" tag.
-
# assert_tag tag: "span", descendant: { tag: "strong" }
-
#
-
# # Assert that there is a "span" containing between 2 and 4 "em" tags
-
# # as immediate children
-
# assert_tag tag: "span",
-
# children: { count: 2..4, only: { tag: "em" } }
-
#
-
# # Get funky: assert that there is a "div", with an "ul" ancestor
-
# # and an "li" parent (with "class" = "enum"), and containing a
-
# # "span" descendant that contains text matching /hello world/
-
# assert_tag tag: "div",
-
# ancestor: { tag: "ul" },
-
# parent: { tag: "li",
-
# attributes: { class: "enum" } },
-
# descendant: { tag: "span",
-
# child: /hello world/ }
-
#
-
# <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work
-
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
-
# (like br and hr and such) but will not work correctly with tags
-
# that allow optional closing tags (p, li, td). <em>You must explicitly
-
# close all of your tags to use these assertions.</em>
-
2
def assert_tag(*opts)
-
ActiveSupport::Deprecation.warn("assert_tag is deprecated and will be removed at Rails 5. Use assert_select to get the same feature.")
-
-
opts = opts.size > 1 ? opts.last.merge({ tag: opts.first.to_s }) : opts.first
-
tag = _find_tag(opts)
-
-
assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
-
end
-
-
# Identical to +assert_tag+, but asserts that a matching tag does _not_
-
# exist. (See +assert_tag+ for a full discussion of the syntax.)
-
#
-
# # Assert that there is not a "div" containing a "p"
-
# assert_no_tag tag: "div", descendant: { tag: "p" }
-
#
-
# # Assert that an unordered list is empty
-
# assert_no_tag tag: "ul", descendant: { tag: "li" }
-
#
-
# # Assert that there is not a "p" tag with between 1 to 3 "img" tags
-
# # as immediate children
-
# assert_no_tag tag: "p",
-
# children: { count: 1..3, only: { tag: "img" } }
-
2
def assert_no_tag(*opts)
-
ActiveSupport::Deprecation.warn("assert_no_tag is deprecated and will be removed at Rails 5. Use assert_select to get the same feature.")
-
-
opts = opts.size > 1 ? opts.last.merge({ tag: opts.first.to_s }) : opts.first
-
tag = _find_tag(opts)
-
-
assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
-
end
-
-
2
def find_tag(conditions)
-
ActiveSupport::Deprecation.warn("find_tag is deprecated and will be removed at Rails 5 without replacement.")
-
-
_find_tag(conditions)
-
end
-
-
2
def find_all_tag(conditions)
-
ActiveSupport::Deprecation.warn("find_all_tag is deprecated and will be removed at Rails 5 without replacement. Use assert_select to get the same feature.")
-
-
html_scanner_document.find_all(conditions)
-
end
-
-
2
private
-
2
def _find_tag(conditions)
-
html_scanner_document.find(conditions)
-
end
-
-
2
def html_scanner_document
-
xml = @response.content_type =~ /xml$/
-
@html_scanner_document ||= HTML::Document.new(@response.body, false, xml)
-
end
-
end
-
end
-
end
-
end
-
end
-
2
require "active_support/notifications"
-
2
require "active_support/dependencies"
-
2
require "active_support/deprecation"
-
2
require "active_support/descendants_tracker"
-
-
2
module Rails
-
2
class Application
-
2
module Bootstrap
-
2
include Initializable
-
-
2
initializer :load_environment_hook, group: :all do end
-
-
2
initializer :load_active_support, group: :all do
-
2
require "active_support/all" unless config.active_support.bare
-
end
-
-
2
initializer :set_eager_load, group: :all do
-
2
if config.eager_load.nil?
-
warn <<-INFO
-
config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly:
-
-
* development - set it to false
-
* test - set it to false (unless you use a tool that preloads your test environment)
-
* production - set it to true
-
-
INFO
-
config.eager_load = config.cache_classes
-
end
-
end
-
-
# Initialize the logger early in the stack in case we need to log some deprecation.
-
2
initializer :initialize_logger, group: :all do
-
2
Rails.logger ||= config.logger || begin
-
2
path = config.paths["log"].first
-
2
unless File.exist? File.dirname path
-
FileUtils.mkdir_p File.dirname path
-
end
-
-
2
f = File.open path, 'a'
-
2
f.binmode
-
2
f.sync = config.autoflush_log # if true make sure every write flushes
-
-
2
logger = ActiveSupport::Logger.new f
-
2
logger.formatter = config.log_formatter
-
2
logger = ActiveSupport::TaggedLogging.new(logger)
-
2
logger
-
rescue StandardError
-
logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR))
-
logger.level = ActiveSupport::Logger::WARN
-
logger.warn(
-
"Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " +
-
"(ie, make it writable for user and group: chmod 0664 #{path}). " +
-
"The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
-
)
-
logger
-
end
-
-
2
if Rails.env.production? && !config.has_explicit_log_level?
-
ActiveSupport::Deprecation.warn \
-
"You did not specify a `log_level` in `production.rb`. Currently, " \
-
"the default value for `log_level` is `:info` for the production " \
-
"environment and `:debug` in all other environments. In Rails 5 " \
-
"the default value will be unified to `:debug` across all " \
-
"environments. To preserve the current setting, add the following " \
-
"line to your `production.rb`:\n" \
-
"\n" \
-
" config.log_level = :info\n\n"
-
end
-
-
2
Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
-
end
-
-
# Initialize cache early in the stack so railties can make use of it.
-
2
initializer :initialize_cache, group: :all do
-
2
unless Rails.cache
-
2
Rails.cache = ActiveSupport::Cache.lookup_store(config.cache_store)
-
-
2
if Rails.cache.respond_to?(:middleware)
-
2
config.middleware.insert_before("Rack::Runtime", Rails.cache.middleware)
-
end
-
end
-
end
-
-
# Sets the dependency loading mechanism.
-
2
initializer :initialize_dependency_mechanism, group: :all do
-
2
ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load
-
end
-
-
2
initializer :bootstrap_hook, group: :all do |app|
-
2
ActiveSupport.run_load_hooks(:before_initialize, app)
-
end
-
end
-
end
-
end
-
2
module Rails
-
2
class Application
-
2
class DefaultMiddlewareStack
-
2
attr_reader :config, :paths, :app
-
-
2
def initialize(app, config, paths)
-
2
@app = app
-
2
@config = config
-
2
@paths = paths
-
end
-
-
2
def build_stack
-
2
ActionDispatch::MiddlewareStack.new.tap do |middleware|
-
2
if config.force_ssl
-
middleware.use ::ActionDispatch::SSL, config.ssl_options
-
end
-
-
2
middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
-
-
2
if config.serve_static_files
-
2
middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control
-
end
-
-
2
if rack_cache = load_rack_cache
-
require "action_dispatch/http/rack_cache"
-
middleware.use ::Rack::Cache, rack_cache
-
end
-
-
2
middleware.use ::Rack::Lock unless allow_concurrency?
-
2
middleware.use ::Rack::Runtime
-
2
middleware.use ::Rack::MethodOverride
-
2
middleware.use ::ActionDispatch::RequestId
-
-
# Must come after Rack::MethodOverride to properly log overridden methods
-
2
middleware.use ::Rails::Rack::Logger, config.log_tags
-
2
middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
-
2
middleware.use ::ActionDispatch::DebugExceptions, app
-
2
middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
-
-
2
unless config.cache_classes
-
middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? }
-
end
-
-
2
middleware.use ::ActionDispatch::Callbacks
-
2
middleware.use ::ActionDispatch::Cookies
-
-
2
if config.session_store
-
2
if config.force_ssl && !config.session_options.key?(:secure)
-
config.session_options[:secure] = true
-
end
-
2
middleware.use config.session_store, config.session_options
-
2
middleware.use ::ActionDispatch::Flash
-
end
-
-
2
middleware.use ::ActionDispatch::ParamsParser
-
2
middleware.use ::Rack::Head
-
2
middleware.use ::Rack::ConditionalGet
-
2
middleware.use ::Rack::ETag, "no-cache"
-
end
-
end
-
-
2
private
-
-
2
def reload_dependencies?
-
config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
-
end
-
-
2
def allow_concurrency?
-
2
if config.allow_concurrency.nil?
-
2
config.cache_classes && config.eager_load
-
else
-
config.allow_concurrency
-
end
-
end
-
-
2
def load_rack_cache
-
2
rack_cache = config.action_dispatch.rack_cache
-
2
return unless rack_cache
-
-
begin
-
require 'rack/cache'
-
rescue LoadError => error
-
error.message << ' Be sure to add rack-cache to your Gemfile'
-
raise
-
end
-
-
if rack_cache == true
-
{
-
metastore: "rails:/",
-
entitystore: "rails:/",
-
verbose: false
-
}
-
else
-
rack_cache
-
end
-
end
-
-
2
def show_exceptions_app
-
2
config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
-
end
-
end
-
end
-
end
-
2
module Rails
-
2
class Application
-
2
module Finisher
-
2
include Initializable
-
-
2
initializer :add_generator_templates do
-
2
config.generators.templates.unshift(*paths["lib/templates"].existent)
-
end
-
-
2
initializer :ensure_autoload_once_paths_as_subset do
-
2
extra = ActiveSupport::Dependencies.autoload_once_paths -
-
ActiveSupport::Dependencies.autoload_paths
-
-
2
unless extra.empty?
-
abort <<-end_error
-
autoload_once_paths must be a subset of the autoload_paths.
-
Extra items in autoload_once_paths: #{extra * ','}
-
end_error
-
end
-
end
-
-
2
initializer :add_builtin_route do |app|
-
2
if Rails.env.development?
-
app.routes.append do
-
get '/rails/info/properties' => "rails/info#properties"
-
get '/rails/info/routes' => "rails/info#routes"
-
get '/rails/info' => "rails/info#index"
-
get '/' => "rails/welcome#index"
-
end
-
end
-
end
-
-
2
initializer :build_middleware_stack do
-
2
build_middleware_stack
-
end
-
-
2
initializer :define_main_app_helper do |app|
-
2
app.routes.define_mounted_helper(:main_app)
-
end
-
-
2
initializer :add_to_prepare_blocks do
-
2
config.to_prepare_blocks.each do |block|
-
ActionDispatch::Reloader.to_prepare(&block)
-
end
-
end
-
-
# This needs to happen before eager load so it happens
-
# in exactly the same point regardless of config.cache_classes
-
2
initializer :run_prepare_callbacks do
-
2
ActionDispatch::Reloader.prepare!
-
end
-
-
2
initializer :eager_load! do
-
2
if config.eager_load
-
ActiveSupport.run_load_hooks(:before_eager_load, self)
-
config.eager_load_namespaces.each(&:eager_load!)
-
end
-
end
-
-
# All initialization is done, including eager loading in production
-
2
initializer :finisher_hook do
-
2
ActiveSupport.run_load_hooks(:after_initialize, self)
-
end
-
-
# Set routes reload after the finisher hook to ensure routes added in
-
# the hook are taken into account.
-
2
initializer :set_routes_reloader_hook do
-
2
reloader = routes_reloader
-
2
reloader.execute_if_updated
-
2
self.reloaders << reloader
-
2
ActionDispatch::Reloader.to_prepare do
-
# We configure #execute rather than #execute_if_updated because if
-
# autoloaded constants are cleared we need to reload routes also in
-
# case any was used there, as in
-
#
-
# mount MailPreview => 'mail_view'
-
#
-
# This means routes are also reloaded if i18n is updated, which
-
# might not be necessary, but in order to be more precise we need
-
# some sort of reloaders dependency support, to be added.
-
reloader.execute
-
end
-
end
-
-
# Set clearing dependencies after the finisher hook to ensure paths
-
# added in the hook are taken into account.
-
2
initializer :set_clear_dependencies_hook, group: :all do
-
2
callback = lambda do
-
ActiveSupport::DescendantsTracker.clear
-
ActiveSupport::Dependencies.clear
-
end
-
-
2
if config.reload_classes_only_on_change
-
2
reloader = config.file_watcher.new(*watchable_args, &callback)
-
2
self.reloaders << reloader
-
-
# Prepend this callback to have autoloaded constants cleared before
-
# any other possible reloading, in case they need to autoload fresh
-
# constants.
-
2
ActionDispatch::Reloader.to_prepare(prepend: true) do
-
# In addition to changes detected by the file watcher, if routes
-
# or i18n have been updated we also need to clear constants,
-
# that's why we run #execute rather than #execute_if_updated, this
-
# callback has to clear autoloaded constants after any update.
-
reloader.execute
-
end
-
else
-
ActionDispatch::Reloader.to_cleanup(&callback)
-
end
-
end
-
end
-
end
-
end
-
2
require "active_support/core_ext/module/delegation"
-
-
2
module Rails
-
2
class Application
-
2
class RoutesReloader
-
2
attr_reader :route_sets, :paths
-
2
delegate :execute_if_updated, :execute, :updated?, to: :updater
-
-
2
def initialize
-
2
@paths = []
-
2
@route_sets = []
-
end
-
-
2
def reload!
-
2
clear!
-
2
load_paths
-
2
finalize!
-
ensure
-
2
revert
-
end
-
-
2
private
-
-
2
def updater
-
@updater ||= begin
-
4
updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! }
-
2
updater.execute
-
2
updater
-
2
end
-
end
-
-
2
def clear!
-
2
route_sets.each do |routes|
-
2
routes.disable_clear_and_finalize = true
-
2
routes.clear!
-
end
-
end
-
-
2
def load_paths
-
4
paths.each { |path| load(path) }
-
end
-
-
2
def finalize!
-
2
route_sets.each do |routes|
-
2
routes.finalize!
-
end
-
end
-
-
2
def revert
-
2
route_sets.each do |routes|
-
2
routes.disable_clear_and_finalize = false
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/backtrace_cleaner'
-
-
2
module Rails
-
2
class BacktraceCleaner < ActiveSupport::BacktraceCleaner
-
2
APP_DIRS_PATTERN = /^\/?(app|config|lib|test)/
-
2
RENDER_TEMPLATE_PATTERN = /:in `_render_template_\w*'/
-
2
EMPTY_STRING = ''.freeze
-
2
SLASH = '/'.freeze
-
2
DOT_SLASH = './'.freeze
-
-
2
def initialize
-
2
super
-
2
@root = "#{Rails.root}/".freeze
-
168
add_filter { |line| line.sub(@root, EMPTY_STRING) }
-
168
add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, EMPTY_STRING) }
-
168
add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests
-
-
2
add_gem_filters
-
168
add_silencer { |line| line !~ APP_DIRS_PATTERN }
-
end
-
-
2
private
-
2
def add_gem_filters
-
6
gems_paths = (Gem.path | [Gem.default_dir]).map { |p| Regexp.escape(p) }
-
2
return if gems_paths.empty?
-
-
2
gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)}
-
2
gems_result = '\2 (\3) \4'.freeze
-
168
add_filter { |line| line.sub(gems_regexp, gems_result) }
-
end
-
end
-
end
-
2
activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__)
-
2
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
-
-
2
require 'thor/group'
-
-
2
require 'active_support'
-
2
require 'active_support/core_ext/object/blank'
-
2
require 'active_support/core_ext/kernel/singleton_class'
-
2
require 'active_support/core_ext/array/extract_options'
-
2
require 'active_support/core_ext/hash/deep_merge'
-
2
require 'active_support/core_ext/module/attribute_accessors'
-
2
require 'active_support/core_ext/string/inflections'
-
-
2
module Rails
-
2
module Generators
-
2
autoload :Actions, 'rails/generators/actions'
-
2
autoload :ActiveModel, 'rails/generators/active_model'
-
2
autoload :Base, 'rails/generators/base'
-
2
autoload :Migration, 'rails/generators/migration'
-
2
autoload :NamedBase, 'rails/generators/named_base'
-
2
autoload :ResourceHelpers, 'rails/generators/resource_helpers'
-
2
autoload :TestCase, 'rails/generators/test_case'
-
-
2
mattr_accessor :namespace
-
-
2
DEFAULT_ALIASES = {
-
rails: {
-
actions: '-a',
-
orm: '-o',
-
javascripts: '-j',
-
javascript_engine: '-je',
-
resource_controller: '-c',
-
scaffold_controller: '-c',
-
stylesheets: '-y',
-
stylesheet_engine: '-se',
-
template_engine: '-e',
-
test_framework: '-t'
-
},
-
-
test_unit: {
-
fixture_replacement: '-r',
-
}
-
}
-
-
2
DEFAULT_OPTIONS = {
-
rails: {
-
assets: true,
-
force_plural: false,
-
helper: true,
-
integration_tool: nil,
-
javascripts: true,
-
javascript_engine: :js,
-
orm: false,
-
resource_controller: :controller,
-
resource_route: true,
-
scaffold_controller: :scaffold_controller,
-
stylesheets: true,
-
stylesheet_engine: :css,
-
test_framework: false,
-
template_engine: :erb
-
}
-
}
-
-
2
def self.configure!(config) #:nodoc:
-
no_color! unless config.colorize_logging
-
aliases.deep_merge! config.aliases
-
options.deep_merge! config.options
-
fallbacks.merge! config.fallbacks
-
templates_path.concat config.templates
-
templates_path.uniq!
-
hide_namespaces(*config.hidden_namespaces)
-
end
-
-
2
def self.templates_path #:nodoc:
-
@templates_path ||= []
-
end
-
-
2
def self.aliases #:nodoc:
-
@aliases ||= DEFAULT_ALIASES.dup
-
end
-
-
2
def self.options #:nodoc:
-
@options ||= DEFAULT_OPTIONS.dup
-
end
-
-
# Hold configured generators fallbacks. If a plugin developer wants a
-
# generator group to fallback to another group in case of missing generators,
-
# they can add a fallback.
-
#
-
# For example, shoulda is considered a test_framework and is an extension
-
# of test_unit. However, most part of shoulda generators are similar to
-
# test_unit ones.
-
#
-
# Shoulda then can tell generators to search for test_unit generators when
-
# some of them are not available by adding a fallback:
-
#
-
# Rails::Generators.fallbacks[:shoulda] = :test_unit
-
2
def self.fallbacks
-
@fallbacks ||= {}
-
end
-
-
# Remove the color from output.
-
2
def self.no_color!
-
2
Thor::Base.shell = Thor::Shell::Basic
-
end
-
-
# Track all generators subclasses.
-
2
def self.subclasses
-
@subclasses ||= []
-
end
-
-
# Rails finds namespaces similar to thor, it only adds one rule:
-
#
-
# Generators names must end with "_generator.rb". This is required because Rails
-
# looks in load paths and loads the generator just before it's going to be used.
-
#
-
# find_by_namespace :webrat, :rails, :integration
-
#
-
# Will search for the following generators:
-
#
-
# "rails:webrat", "webrat:integration", "webrat"
-
#
-
# Notice that "rails:generators:webrat" could be loaded as well, what
-
# Rails looks for is the first and last parts of the namespace.
-
2
def self.find_by_namespace(name, base=nil, context=nil) #:nodoc:
-
lookups = []
-
lookups << "#{base}:#{name}" if base
-
lookups << "#{name}:#{context}" if context
-
-
unless base || context
-
unless name.to_s.include?(?:)
-
lookups << "#{name}:#{name}"
-
lookups << "rails:#{name}"
-
end
-
lookups << "#{name}"
-
end
-
-
lookup(lookups)
-
-
namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
-
-
lookups.each do |namespace|
-
klass = namespaces[namespace]
-
return klass if klass
-
end
-
-
invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
-
end
-
-
# Receives a namespace, arguments and the behavior to invoke the generator.
-
# It's used as the default entry point for generate, destroy and update
-
# commands.
-
2
def self.invoke(namespace, args=ARGV, config={})
-
names = namespace.to_s.split(':')
-
if klass = find_by_namespace(names.pop, names.any? && names.join(':'))
-
args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
-
klass.start(args, config)
-
else
-
options = sorted_groups.map(&:last).flatten
-
suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3)
-
msg = "Could not find generator '#{namespace}'. "
-
msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.to_sentence(last_word_connector: " or ") }\n"
-
msg << "Run `rails generate --help` for more options."
-
puts msg
-
end
-
end
-
-
# Returns an array of generator namespaces that are hidden.
-
# Generator namespaces may be hidden for a variety of reasons.
-
# Some are aliased such as "rails:migration" and can be
-
# invoked with the shorter "migration", others are private to other generators
-
# such as "css:scaffold".
-
2
def self.hidden_namespaces
-
@hidden_namespaces ||= begin
-
orm = options[:rails][:orm]
-
test = options[:rails][:test_framework]
-
template = options[:rails][:template_engine]
-
css = options[:rails][:stylesheet_engine]
-
-
[
-
"rails",
-
"resource_route",
-
"#{orm}:migration",
-
"#{orm}:model",
-
"#{test}:controller",
-
"#{test}:helper",
-
"#{test}:integration",
-
"#{test}:mailer",
-
"#{test}:model",
-
"#{test}:scaffold",
-
"#{test}:view",
-
"#{template}:controller",
-
"#{template}:scaffold",
-
"#{template}:mailer",
-
"#{css}:scaffold",
-
"#{css}:assets",
-
"css:assets",
-
"css:scaffold"
-
]
-
end
-
end
-
-
2
class << self
-
2
def hide_namespaces(*namespaces)
-
hidden_namespaces.concat(namespaces)
-
end
-
2
alias hide_namespace hide_namespaces
-
end
-
-
# Show help message with available generators.
-
2
def self.help(command = 'generate')
-
puts "Usage: rails #{command} GENERATOR [args] [options]"
-
puts
-
puts "General options:"
-
puts " -h, [--help] # Print generator's options and usage"
-
puts " -p, [--pretend] # Run but do not make any changes"
-
puts " -f, [--force] # Overwrite files that already exist"
-
puts " -s, [--skip] # Skip files that already exist"
-
puts " -q, [--quiet] # Suppress status output"
-
puts
-
puts "Please choose a generator below."
-
puts
-
-
print_generators
-
end
-
-
2
def self.public_namespaces
-
lookup!
-
subclasses.map { |k| k.namespace }
-
end
-
-
2
def self.print_generators
-
sorted_groups.each { |b, n| print_list(b, n) }
-
end
-
-
2
def self.sorted_groups
-
namespaces = public_namespaces
-
namespaces.sort!
-
groups = Hash.new { |h,k| h[k] = [] }
-
namespaces.each do |namespace|
-
base = namespace.split(':').first
-
groups[base] << namespace
-
end
-
rails = groups.delete("rails")
-
rails.map! { |n| n.sub(/^rails:/, '') }
-
rails.delete("app")
-
rails.delete("plugin")
-
-
hidden_namespaces.each { |n| groups.delete(n.to_s) }
-
-
[["rails", rails]] + groups.sort.to_a
-
end
-
-
2
protected
-
-
# This code is based directly on the Text gem implementation
-
# Returns a value representing the "cost" of transforming str1 into str2
-
2
def self.levenshtein_distance str1, str2
-
s = str1
-
t = str2
-
n = s.length
-
m = t.length
-
-
return m if (0 == n)
-
return n if (0 == m)
-
-
d = (0..m).to_a
-
x = nil
-
-
str1.each_char.each_with_index do |char1,i|
-
e = i+1
-
-
str2.each_char.each_with_index do |char2,j|
-
cost = (char1 == char2) ? 0 : 1
-
x = [
-
d[j+1] + 1, # insertion
-
e + 1, # deletion
-
d[j] + cost # substitution
-
].min
-
d[j] = e
-
e = x
-
end
-
-
d[m] = x
-
end
-
-
return x
-
end
-
-
# Prints a list of generators.
-
2
def self.print_list(base, namespaces) #:nodoc:
-
namespaces = namespaces.reject do |n|
-
hidden_namespaces.include?(n)
-
end
-
-
return if namespaces.empty?
-
puts "#{base.camelize}:"
-
-
namespaces.each do |namespace|
-
puts(" #{namespace}")
-
end
-
-
puts
-
end
-
-
# Try fallbacks for the given base.
-
2
def self.invoke_fallbacks_for(name, base) #:nodoc:
-
return nil unless base && fallbacks[base.to_sym]
-
invoked_fallbacks = []
-
-
Array(fallbacks[base.to_sym]).each do |fallback|
-
next if invoked_fallbacks.include?(fallback)
-
invoked_fallbacks << fallback
-
-
klass = find_by_namespace(name, fallback)
-
return klass if klass
-
end
-
-
nil
-
end
-
-
# Receives namespaces in an array and tries to find matching generators
-
# in the load path.
-
2
def self.lookup(namespaces) #:nodoc:
-
paths = namespaces_to_paths(namespaces)
-
-
paths.each do |raw_path|
-
["rails/generators", "generators"].each do |base|
-
path = "#{base}/#{raw_path}_generator"
-
-
begin
-
require path
-
return
-
rescue LoadError => e
-
raise unless e.message =~ /#{Regexp.escape(path)}$/
-
rescue Exception => e
-
warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
-
end
-
end
-
end
-
end
-
-
# This will try to load any generator in the load path to show in help.
-
2
def self.lookup! #:nodoc:
-
$LOAD_PATH.each do |base|
-
Dir[File.join(base, "{rails/generators,generators}", "**", "*_generator.rb")].each do |path|
-
begin
-
path = path.sub("#{base}/", "")
-
require path
-
rescue Exception
-
# No problem
-
end
-
end
-
end
-
end
-
-
# Convert namespaces to paths by replacing ":" for "/" and adding
-
# an extra lookup. For example, "rails:model" should be searched
-
# in both: "rails/model/model_generator" and "rails/model_generator".
-
2
def self.namespaces_to_paths(namespaces) #:nodoc:
-
paths = []
-
namespaces.each do |namespace|
-
pieces = namespace.split(":")
-
paths << pieces.dup.push(pieces.last).join("/")
-
paths << pieces.join("/")
-
end
-
paths.uniq!
-
paths
-
end
-
end
-
end
-
2
require 'rails/generators'
-
2
require 'rails/generators/testing/behaviour'
-
2
require 'rails/generators/testing/setup_and_teardown'
-
2
require 'rails/generators/testing/assertions'
-
2
require 'fileutils'
-
-
2
module Rails
-
2
module Generators
-
# Disable color in output. Easier to debug.
-
2
no_color!
-
-
# This class provides a TestCase for testing generators. To setup, you need
-
# just to configure the destination and set which generator is being tested:
-
#
-
# class AppGeneratorTest < Rails::Generators::TestCase
-
# tests AppGenerator
-
# destination File.expand_path("../tmp", File.dirname(__FILE__))
-
# end
-
#
-
# If you want to ensure your destination root is clean before running each test,
-
# you can set a setup callback:
-
#
-
# class AppGeneratorTest < Rails::Generators::TestCase
-
# tests AppGenerator
-
# destination File.expand_path("../tmp", File.dirname(__FILE__))
-
# setup :prepare_destination
-
# end
-
2
class TestCase < ActiveSupport::TestCase
-
2
include Rails::Generators::Testing::Behaviour
-
2
include Rails::Generators::Testing::SetupAndTeardown
-
2
include Rails::Generators::Testing::Assertions
-
2
include FileUtils
-
-
end
-
end
-
end
-
2
require 'shellwords'
-
-
2
module Rails
-
2
module Generators
-
2
module Testing
-
2
module Assertions
-
# Asserts a given file exists. You need to supply an absolute path or a path relative
-
# to the configured destination:
-
#
-
# assert_file "config/environment.rb"
-
#
-
# You can also give extra arguments. If the argument is a regexp, it will check if the
-
# regular expression matches the given file content. If it's a string, it compares the
-
# file with the given string:
-
#
-
# assert_file "config/environment.rb", /initialize/
-
#
-
# Finally, when a block is given, it yields the file content:
-
#
-
# assert_file "app/controllers/products_controller.rb" do |controller|
-
# assert_instance_method :index, controller do |index|
-
# assert_match(/Product\.all/, index)
-
# end
-
# end
-
2
def assert_file(relative, *contents)
-
absolute = File.expand_path(relative, destination_root)
-
assert File.exist?(absolute), "Expected file #{relative.inspect} to exist, but does not"
-
-
read = File.read(absolute) if block_given? || !contents.empty?
-
yield read if block_given?
-
-
contents.each do |content|
-
case content
-
when String
-
assert_equal content, read
-
when Regexp
-
assert_match content, read
-
end
-
end
-
end
-
2
alias :assert_directory :assert_file
-
-
# Asserts a given file does not exist. You need to supply an absolute path or a
-
# path relative to the configured destination:
-
#
-
# assert_no_file "config/random.rb"
-
2
def assert_no_file(relative)
-
absolute = File.expand_path(relative, destination_root)
-
assert !File.exist?(absolute), "Expected file #{relative.inspect} to not exist, but does"
-
end
-
2
alias :assert_no_directory :assert_no_file
-
-
# Asserts a given migration exists. You need to supply an absolute path or a
-
# path relative to the configured destination:
-
#
-
# assert_migration "db/migrate/create_products.rb"
-
#
-
# This method manipulates the given path and tries to find any migration which
-
# matches the migration name. For example, the call above is converted to:
-
#
-
# assert_file "db/migrate/003_create_products.rb"
-
#
-
# Consequently, assert_migration accepts the same arguments has assert_file.
-
2
def assert_migration(relative, *contents, &block)
-
file_name = migration_file_name(relative)
-
assert file_name, "Expected migration #{relative} to exist, but was not found"
-
assert_file file_name, *contents, &block
-
end
-
-
# Asserts a given migration does not exist. You need to supply an absolute path or a
-
# path relative to the configured destination:
-
#
-
# assert_no_migration "db/migrate/create_products.rb"
-
2
def assert_no_migration(relative)
-
file_name = migration_file_name(relative)
-
assert_nil file_name, "Expected migration #{relative} to not exist, but found #{file_name}"
-
end
-
-
# Asserts the given class method exists in the given content. This method does not detect
-
# class methods inside (class << self), only class methods which starts with "self.".
-
# When a block is given, it yields the content of the method.
-
#
-
# assert_migration "db/migrate/create_products.rb" do |migration|
-
# assert_class_method :up, migration do |up|
-
# assert_match(/create_table/, up)
-
# end
-
# end
-
2
def assert_class_method(method, content, &block)
-
assert_instance_method "self.#{method}", content, &block
-
end
-
-
# Asserts the given method exists in the given content. When a block is given,
-
# it yields the content of the method.
-
#
-
# assert_file "app/controllers/products_controller.rb" do |controller|
-
# assert_instance_method :index, controller do |index|
-
# assert_match(/Product\.all/, index)
-
# end
-
# end
-
2
def assert_instance_method(method, content)
-
assert content =~ /(\s+)def #{method}(\(.+\))?(.*?)\n\1end/m, "Expected to have method #{method}"
-
yield $3.strip if block_given?
-
end
-
2
alias :assert_method :assert_instance_method
-
-
# Asserts the given attribute type gets translated to a field type
-
# properly:
-
#
-
# assert_field_type :date, :date_select
-
2
def assert_field_type(attribute_type, field_type)
-
assert_equal(field_type, create_generated_attribute(attribute_type).field_type)
-
end
-
-
# Asserts the given attribute type gets a proper default value:
-
#
-
# assert_field_default_value :string, "MyString"
-
2
def assert_field_default_value(attribute_type, value)
-
assert_equal(value, create_generated_attribute(attribute_type).default)
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/class/attribute'
-
2
require 'active_support/core_ext/module/delegation'
-
2
require 'active_support/core_ext/hash/reverse_merge'
-
2
require 'active_support/core_ext/kernel/reporting'
-
2
require 'active_support/concern'
-
2
require 'rails/generators'
-
-
2
module Rails
-
2
module Generators
-
2
module Testing
-
2
module Behaviour
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :destination_root, :current_path, :generator_class, :default_arguments
-
-
# Generators frequently change the current path using +FileUtils.cd+.
-
# So we need to store the path at file load and revert back to it after each test.
-
2
self.current_path = File.expand_path(Dir.pwd)
-
2
self.default_arguments = []
-
end
-
-
2
module ClassMethods
-
# Sets which generator should be tested:
-
#
-
# tests AppGenerator
-
2
def tests(klass)
-
self.generator_class = klass
-
end
-
-
# Sets default arguments on generator invocation. This can be overwritten when
-
# invoking it.
-
#
-
# arguments %w(app_name --skip-active-record)
-
2
def arguments(array)
-
self.default_arguments = array
-
end
-
-
# Sets the destination of generator files:
-
#
-
# destination File.expand_path("../tmp", File.dirname(__FILE__))
-
2
def destination(path)
-
self.destination_root = path
-
end
-
end
-
-
# Runs the generator configured for this class. The first argument is an array like
-
# command line arguments:
-
#
-
# class AppGeneratorTest < Rails::Generators::TestCase
-
# tests AppGenerator
-
# destination File.expand_path("../tmp", File.dirname(__FILE__))
-
# setup :prepare_destination
-
#
-
# test "database.yml is not created when skipping Active Record" do
-
# run_generator %w(myapp --skip-active-record)
-
# assert_no_file "config/database.yml"
-
# end
-
# end
-
#
-
# You can provide a configuration hash as second argument. This method returns the output
-
# printed by the generator.
-
2
def run_generator(args=self.default_arguments, config={})
-
capture(:stdout) do
-
args += ['--skip-bundle'] unless args.include? '--dev'
-
self.generator_class.start(args, config.reverse_merge(destination_root: destination_root))
-
end
-
end
-
-
# Instantiate the generator.
-
2
def generator(args=self.default_arguments, options={}, config={})
-
@generator ||= self.generator_class.new(args, options, config.reverse_merge(destination_root: destination_root))
-
end
-
-
# Create a Rails::Generators::GeneratedAttribute by supplying the
-
# attribute type and, optionally, the attribute name:
-
#
-
# create_generated_attribute(:string, 'name')
-
2
def create_generated_attribute(attribute_type, name = 'test', index = nil)
-
Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(':'))
-
end
-
-
2
protected
-
-
2
def destination_root_is_set? # :nodoc:
-
raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root
-
end
-
-
2
def ensure_current_path # :nodoc:
-
cd current_path
-
end
-
-
2
def prepare_destination # :nodoc:
-
rm_rf(destination_root)
-
mkdir_p(destination_root)
-
end
-
-
2
def migration_file_name(relative) # :nodoc:
-
absolute = File.expand_path(relative, destination_root)
-
dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '')
-
Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
-
end
-
-
2
def capture(stream)
-
stream = stream.to_s
-
captured_stream = Tempfile.new(stream)
-
stream_io = eval("$#{stream}")
-
origin_stream = stream_io.dup
-
stream_io.reopen(captured_stream)
-
-
yield
-
-
stream_io.rewind
-
return captured_stream.read
-
ensure
-
captured_stream.close
-
captured_stream.unlink
-
stream_io.reopen(origin_stream)
-
end
-
end
-
end
-
end
-
end
-
2
module Rails
-
2
module Generators
-
2
module Testing
-
2
module SetupAndTeardown
-
2
def setup # :nodoc:
-
destination_root_is_set?
-
ensure_current_path
-
super
-
end
-
-
2
def teardown # :nodoc:
-
ensure_current_path
-
super
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/core_ext/time/conversions'
-
2
require 'active_support/core_ext/object/blank'
-
2
require 'active_support/log_subscriber'
-
2
require 'action_dispatch/http/request'
-
2
require 'rack/body_proxy'
-
-
2
module Rails
-
2
module Rack
-
# Sets log tags, logs the request, calls the app, and flushes the logs.
-
2
class Logger < ActiveSupport::LogSubscriber
-
2
def initialize(app, taggers = nil)
-
2
@app = app
-
2
@taggers = taggers || []
-
end
-
-
2
def call(env)
-
request = ActionDispatch::Request.new(env)
-
-
if logger.respond_to?(:tagged)
-
logger.tagged(compute_tags(request)) { call_app(request, env) }
-
else
-
call_app(request, env)
-
end
-
end
-
-
2
protected
-
-
2
def call_app(request, env)
-
# Put some space between requests in development logs.
-
if development?
-
logger.debug ''
-
logger.debug ''
-
end
-
-
instrumenter = ActiveSupport::Notifications.instrumenter
-
instrumenter.start 'request.action_dispatch', request: request
-
logger.info { started_request_message(request) }
-
resp = @app.call(env)
-
resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
-
resp
-
rescue Exception
-
finish(request)
-
raise
-
ensure
-
ActiveSupport::LogSubscriber.flush_all!
-
end
-
-
# Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700
-
2
def started_request_message(request)
-
'Started %s "%s" for %s at %s' % [
-
request.request_method,
-
request.filtered_path,
-
request.ip,
-
Time.now.to_default_s ]
-
end
-
-
2
def compute_tags(request)
-
@taggers.collect do |tag|
-
case tag
-
when Proc
-
tag.call(request)
-
when Symbol
-
request.send(tag)
-
else
-
tag
-
end
-
end
-
end
-
-
2
private
-
-
2
def finish(request)
-
instrumenter = ActiveSupport::Notifications.instrumenter
-
instrumenter.finish 'request.action_dispatch', request: request
-
end
-
-
2
def development?
-
Rails.env.development?
-
end
-
-
2
def logger
-
Rails.logger
-
end
-
end
-
end
-
end
-
# Make double-sure the RAILS_ENV is not set to production,
-
# so fixtures aren't loaded into that environment
-
2
abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production?
-
-
2
require 'active_support/testing/autorun'
-
2
require 'active_support/test_case'
-
2
require 'action_controller'
-
2
require 'action_controller/test_case'
-
2
require 'action_dispatch/testing/integration'
-
2
require 'rails/generators/test_case'
-
-
# Config Rails backtrace in tests.
-
2
require 'rails/backtrace_cleaner'
-
2
if ENV["BACKTRACE"].nil?
-
2
Minitest.backtrace_filter = Rails.backtrace_cleaner
-
end
-
-
2
if defined?(ActiveRecord::Base)
-
2
ActiveRecord::Migration.maintain_test_schema!
-
-
2
class ActiveSupport::TestCase
-
2
include ActiveRecord::TestFixtures
-
2
self.fixture_path = "#{Rails.root}/test/fixtures/"
-
end
-
-
2
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
-
-
2
def create_fixtures(*fixture_set_names, &block)
-
FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, {}, &block)
-
end
-
end
-
-
2
class ActionController::TestCase
-
2
setup do
-
29
@routes = Rails.application.routes
-
end
-
end
-
-
2
class ActionDispatch::IntegrationTest
-
2
setup do
-
@routes = Rails.application.routes
-
end
-
end
-
2
require 'active_support/core_ext/array/extract_options'
-
2
require 'action_controller/metal/mime_responds'
-
-
2
module ActionController #:nodoc:
-
2
module RespondWith
-
2
extend ActiveSupport::Concern
-
-
2
included do
-
2
class_attribute :responder, :mimes_for_respond_to
-
2
self.responder = ActionController::Responder
-
2
clear_respond_to
-
end
-
-
2
module ClassMethods
-
# Defines mime types that are rendered by default when invoking
-
# <tt>respond_with</tt>.
-
#
-
# respond_to :html, :xml, :json
-
#
-
# Specifies that all actions in the controller respond to requests
-
# for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
-
#
-
# To specify on per-action basis, use <tt>:only</tt> and
-
# <tt>:except</tt> with an array of actions or a single action:
-
#
-
# respond_to :html
-
# respond_to :xml, :json, except: [ :edit ]
-
#
-
# This specifies that all actions respond to <tt>:html</tt>
-
# and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and
-
# <tt>:json</tt>.
-
#
-
# respond_to :json, only: :create
-
#
-
# This specifies that the <tt>:create</tt> action and no other responds
-
# to <tt>:json</tt>.
-
2
def respond_to(*mimes)
-
3
options = mimes.extract_options!
-
-
3
only_actions = Array(options.delete(:only)).map(&:to_s)
-
3
except_actions = Array(options.delete(:except)).map(&:to_s)
-
-
3
hash = mimes_for_respond_to.dup
-
3
mimes.each do |mime|
-
5
mime = mime.to_sym
-
5
hash[mime] = {}
-
5
hash[mime][:only] = only_actions unless only_actions.empty?
-
5
hash[mime][:except] = except_actions unless except_actions.empty?
-
end
-
3
self.mimes_for_respond_to = hash.freeze
-
end
-
-
# Clear all mime types in <tt>respond_to</tt>.
-
#
-
2
def clear_respond_to
-
2
self.mimes_for_respond_to = Hash.new.freeze
-
end
-
end
-
-
# For a given controller action, respond_with generates an appropriate
-
# response based on the mime-type requested by the client.
-
#
-
# If the method is called with just a resource, as in this example -
-
#
-
# class PeopleController < ApplicationController
-
# respond_to :html, :xml, :json
-
#
-
# def index
-
# @people = Person.all
-
# respond_with @people
-
# end
-
# end
-
#
-
# then the mime-type of the response is typically selected based on the
-
# request's Accept header and the set of available formats declared
-
# by previous calls to the controller's class method +respond_to+. Alternatively
-
# the mime-type can be selected by explicitly setting <tt>request.format</tt> in
-
# the controller.
-
#
-
# If an acceptable format is not identified, the application returns a
-
# '406 - not acceptable' status. Otherwise, the default response is to render
-
# a template named after the current action and the selected format,
-
# e.g. <tt>index.html.erb</tt>. If no template is available, the behavior
-
# depends on the selected format:
-
#
-
# * for an html response - if the request method is +get+, an exception
-
# is raised but for other requests such as +post+ the response
-
# depends on whether the resource has any validation errors (i.e.
-
# assuming that an attempt has been made to save the resource,
-
# e.g. by a +create+ action) -
-
# 1. If there are no errors, i.e. the resource
-
# was saved successfully, the response +redirect+'s to the resource
-
# i.e. its +show+ action.
-
# 2. If there are validation errors, the response
-
# renders a default action, which is <tt>:new</tt> for a
-
# +post+ request or <tt>:edit</tt> for +patch+ or +put+.
-
# Thus an example like this -
-
#
-
# respond_to :html, :xml
-
#
-
# def create
-
# @user = User.new(params[:user])
-
# flash[:notice] = 'User was successfully created.' if @user.save
-
# respond_with(@user)
-
# end
-
#
-
# is equivalent, in the absence of <tt>create.html.erb</tt>, to -
-
#
-
# def create
-
# @user = User.new(params[:user])
-
# respond_to do |format|
-
# if @user.save
-
# flash[:notice] = 'User was successfully created.'
-
# format.html { redirect_to(@user) }
-
# format.xml { render xml: @user }
-
# else
-
# format.html { render action: "new" }
-
# format.xml { render xml: @user }
-
# end
-
# end
-
# end
-
#
-
# * for a JavaScript request - if the template isn't found, an exception is
-
# raised.
-
# * for other requests - i.e. data formats such as xml, json, csv etc, if
-
# the resource passed to +respond_with+ responds to <code>to_<format></code>,
-
# the method attempts to render the resource in the requested format
-
# directly, e.g. for an xml request, the response is equivalent to calling
-
# <code>render xml: resource</code>.
-
#
-
# === Nested resources
-
#
-
# As outlined above, the +resources+ argument passed to +respond_with+
-
# can play two roles. It can be used to generate the redirect url
-
# for successful html requests (e.g. for +create+ actions when
-
# no template exists), while for formats other than html and JavaScript
-
# it is the object that gets rendered, by being converted directly to the
-
# required format (again assuming no template exists).
-
#
-
# For redirecting successful html requests, +respond_with+ also supports
-
# the use of nested resources, which are supplied in the same way as
-
# in <code>form_for</code> and <code>polymorphic_url</code>. For example -
-
#
-
# def create
-
# @project = Project.find(params[:project_id])
-
# @task = @project.comments.build(params[:task])
-
# flash[:notice] = 'Task was successfully created.' if @task.save
-
# respond_with(@project, @task)
-
# end
-
#
-
# This would cause +respond_with+ to redirect to <code>project_task_url</code>
-
# instead of <code>task_url</code>. For request formats other than html or
-
# JavaScript, if multiple resources are passed in this way, it is the last
-
# one specified that is rendered.
-
#
-
# === Customizing response behavior
-
#
-
# Like +respond_to+, +respond_with+ may also be called with a block that
-
# can be used to overwrite any of the default responses, e.g. -
-
#
-
# def create
-
# @user = User.new(params[:user])
-
# flash[:notice] = "User was successfully created." if @user.save
-
#
-
# respond_with(@user) do |format|
-
# format.html { render }
-
# end
-
# end
-
#
-
# The argument passed to the block is an ActionController::MimeResponds::Collector
-
# object which stores the responses for the formats defined within the
-
# block. Note that formats with responses defined explicitly in this way
-
# do not have to first be declared using the class method +respond_to+.
-
#
-
# Also, a hash passed to +respond_with+ immediately after the specified
-
# resource(s) is interpreted as a set of options relevant to all
-
# formats. Any option accepted by +render+ can be used, e.g.
-
#
-
# respond_with @people, status: 200
-
#
-
# However, note that these options are ignored after an unsuccessful attempt
-
# to save a resource, e.g. when automatically rendering <tt>:new</tt>
-
# after a post request.
-
#
-
# Two additional options are relevant specifically to +respond_with+ -
-
# 1. <tt>:location</tt> - overwrites the default redirect location used after
-
# a successful html +post+ request.
-
# 2. <tt>:action</tt> - overwrites the default render action used after an
-
# unsuccessful html +post+ request.
-
2
def respond_with(*resources, &block)
-
if self.class.mimes_for_respond_to.empty?
-
raise "In order to use respond_with, first you need to declare the " \
-
"formats your controller responds to in the class level."
-
end
-
-
mimes = collect_mimes_from_class_level
-
collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
-
block.call(collector) if block_given?
-
-
if format = collector.negotiate_format(request)
-
_process_format(format)
-
options = resources.size == 1 ? {} : resources.extract_options!
-
options = options.clone
-
options[:default_response] = collector.response
-
(options.delete(:responder) || self.class.responder).call(self, resources, options)
-
else
-
raise ActionController::UnknownFormat
-
end
-
end
-
-
2
protected
-
-
# Before action callback that can be used to prevent requests that do not
-
# match the mime types defined through <tt>respond_to</tt> from being executed.
-
#
-
# class PeopleController < ApplicationController
-
# respond_to :html, :xml, :json
-
#
-
# before_action :verify_request_format!
-
# end
-
2
def verify_request_format!
-
mimes = collect_mimes_from_class_level
-
collector = ActionController::MimeResponds::Collector.new(mimes, request.variant)
-
-
unless collector.negotiate_format(request)
-
raise ActionController::UnknownFormat
-
end
-
end
-
-
# Collect mimes declared in the class method respond_to valid for the
-
# current action.
-
2
def collect_mimes_from_class_level #:nodoc:
-
action = action_name.to_s
-
-
self.class.mimes_for_respond_to.keys.select do |mime|
-
config = self.class.mimes_for_respond_to[mime]
-
-
if config[:except]
-
!config[:except].include?(action)
-
elsif config[:only]
-
config[:only].include?(action)
-
else
-
true
-
end
-
end
-
end
-
end
-
end
-
2
require 'active_support/json'
-
-
2
module ActionController #:nodoc:
-
# Responsible for exposing a resource to different mime requests,
-
# usually depending on the HTTP verb. The responder is triggered when
-
# <code>respond_with</code> is called. The simplest case to study is a GET request:
-
#
-
# class PeopleController < ApplicationController
-
# respond_to :html, :xml, :json
-
#
-
# def index
-
# @people = Person.all
-
# respond_with(@people)
-
# end
-
# end
-
#
-
# When a request comes in, for example for an XML response, three steps happen:
-
#
-
# 1) the responder searches for a template at people/index.xml;
-
#
-
# 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
-
#
-
# 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
-
#
-
# === Built-in HTTP verb semantics
-
#
-
# The default \Rails responder holds semantics for each HTTP verb. Depending on the
-
# content type, verb and the resource status, it will behave differently.
-
#
-
# Using \Rails default responder, a POST request for creating an object could
-
# be written as:
-
#
-
# def create
-
# @user = User.new(params[:user])
-
# flash[:notice] = 'User was successfully created.' if @user.save
-
# respond_with(@user)
-
# end
-
#
-
# Which is exactly the same as:
-
#
-
# def create
-
# @user = User.new(params[:user])
-
#
-
# respond_to do |format|
-
# if @user.save
-
# flash[:notice] = 'User was successfully created.'
-
# format.html { redirect_to(@user) }
-
# format.xml { render xml: @user, status: :created, location: @user }
-
# else
-
# format.html { render action: "new" }
-
# format.xml { render xml: @user.errors, status: :unprocessable_entity }
-
# end
-
# end
-
# end
-
#
-
# The same happens for PATCH/PUT and DELETE requests.
-
#
-
# === Nested resources
-
#
-
# You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>.
-
# Consider the project has many tasks example. The create action for
-
# TasksController would be like:
-
#
-
# def create
-
# @project = Project.find(params[:project_id])
-
# @task = @project.tasks.build(params[:task])
-
# flash[:notice] = 'Task was successfully created.' if @task.save
-
# respond_with(@project, @task)
-
# end
-
#
-
# Giving several resources ensures that the responder will redirect to
-
# <code>project_task_url</code> instead of <code>task_url</code>.
-
#
-
# Namespaced and singleton resources require a symbol to be given, as in
-
# polymorphic urls. If a project has one manager which has many tasks, it
-
# should be invoked as:
-
#
-
# respond_with(@project, :manager, @task)
-
#
-
# Note that if you give an array, it will be treated as a collection,
-
# so the following is not equivalent:
-
#
-
# respond_with [@project, :manager, @task]
-
#
-
# === Custom options
-
#
-
# <code>respond_with</code> also allows you to pass options that are forwarded
-
# to the underlying render call. Those options are only applied for success
-
# scenarios. For instance, you can do the following in the create method above:
-
#
-
# def create
-
# @project = Project.find(params[:project_id])
-
# @task = @project.tasks.build(params[:task])
-
# flash[:notice] = 'Task was successfully created.' if @task.save
-
# respond_with(@project, @task, status: 201)
-
# end
-
#
-
# This will return status 201 if the task was saved successfully. If not,
-
# it will simply ignore the given options and return status 422 and the
-
# resource errors. You can also override the location to redirect to:
-
#
-
# respond_with(@project, location: root_path)
-
#
-
# To customize the failure scenario, you can pass a block to
-
# <code>respond_with</code>:
-
#
-
# def create
-
# @project = Project.find(params[:project_id])
-
# @task = @project.tasks.build(params[:task])
-
# respond_with(@project, @task, status: 201) do |format|
-
# if @task.save
-
# flash[:notice] = 'Task was successfully created.'
-
# else
-
# format.html { render "some_special_template" }
-
# end
-
# end
-
# end
-
#
-
# Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
-
2
class Responder
-
2
attr_reader :controller, :request, :format, :resource, :resources, :options
-
-
2
DEFAULT_ACTIONS_FOR_VERBS = {
-
:post => :new,
-
:patch => :edit,
-
:put => :edit
-
}
-
-
2
def initialize(controller, resources, options={})
-
@controller = controller
-
@request = @controller.request
-
@format = @controller.formats.first
-
@resource = resources.last
-
@resources = resources
-
@options = options
-
@action = options.delete(:action)
-
@default_response = options.delete(:default_response)
-
-
if options[:location].respond_to?(:call)
-
location = options.delete(:location)
-
options[:location] = location.call unless has_errors?
-
end
-
end
-
-
2
delegate :head, :render, :redirect_to, :to => :controller
-
2
delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request
-
-
# Undefine :to_json and :to_yaml since it's defined on Object
-
2
undef_method(:to_json) if method_defined?(:to_json)
-
2
undef_method(:to_yaml) if method_defined?(:to_yaml)
-
-
# Initializes a new responder and invokes the proper format. If the format is
-
# not defined, call to_format.
-
#
-
2
def self.call(*args)
-
new(*args).respond
-
end
-
-
# Main entry point for responder responsible to dispatch to the proper format.
-
#
-
2
def respond
-
method = "to_#{format}"
-
respond_to?(method) ? send(method) : to_format
-
end
-
-
# HTML format does not render the resource, it always attempt to render a
-
# template.
-
#
-
2
def to_html
-
default_render
-
rescue ActionView::MissingTemplate => e
-
navigation_behavior(e)
-
end
-
-
# to_js simply tries to render a template. If no template is found, raises the error.
-
2
def to_js
-
default_render
-
end
-
-
# All other formats follow the procedure below. First we try to render a
-
# template, if the template is not available, we verify if the resource
-
# responds to :to_format and display it.
-
#
-
2
def to_format
-
if !get? && has_errors? && !response_overridden?
-
display_errors
-
elsif has_view_rendering? || response_overridden?
-
default_render
-
else
-
api_behavior
-
end
-
rescue ActionView::MissingTemplate
-
api_behavior
-
end
-
-
2
protected
-
-
# This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
-
2
def navigation_behavior(error)
-
if get?
-
raise error
-
elsif has_errors? && default_action
-
render :action => default_action
-
else
-
redirect_to navigation_location
-
end
-
end
-
-
# This is the common behavior for formats associated with APIs, such as :xml and :json.
-
2
def api_behavior
-
raise MissingRenderer.new(format) unless has_renderer?
-
-
if get?
-
display resource
-
elsif post?
-
display resource, :status => :created, :location => api_location
-
else
-
head :no_content
-
end
-
end
-
-
# Returns the resource location by retrieving it from the options or
-
# returning the resources array.
-
#
-
2
def resource_location
-
options[:location] || resources
-
end
-
2
alias :navigation_location :resource_location
-
2
alias :api_location :resource_location
-
-
# If a response block was given, use it, otherwise call render on
-
# controller.
-
#
-
2
def default_render
-
if @default_response
-
@default_response.call(options)
-
else
-
controller.render(options)
-
end
-
end
-
-
# Display is just a shortcut to render a resource with the current format.
-
#
-
# display @user, status: :ok
-
#
-
# For XML requests it's equivalent to:
-
#
-
# render xml: @user, status: :ok
-
#
-
# Options sent by the user are also used:
-
#
-
# respond_with(@user, status: :created)
-
# display(@user, status: :ok)
-
#
-
# Results in:
-
#
-
# render xml: @user, status: :created
-
#
-
2
def display(resource, given_options={})
-
controller.render given_options.merge!(options).merge!(format => resource)
-
end
-
-
2
def display_errors
-
controller.render format => resource_errors, :status => :unprocessable_entity
-
end
-
-
# Check whether the resource has errors.
-
#
-
2
def has_errors?
-
resource.respond_to?(:errors) && !resource.errors.empty?
-
end
-
-
# Check whether the necessary Renderer is available
-
2
def has_renderer?
-
Renderers::RENDERERS.include?(format)
-
end
-
-
2
def has_view_rendering?
-
controller.class.include? ActionView::Rendering
-
end
-
-
# By default, render the <code>:edit</code> action for HTML requests with errors, unless
-
# the verb was POST.
-
#
-
2
def default_action
-
@action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
-
end
-
-
2
def resource_errors
-
respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
-
end
-
-
2
def json_resource_errors
-
{:errors => resource.errors}
-
end
-
-
2
def response_overridden?
-
@default_response.present?
-
end
-
end
-
end
-
2
module Responders
-
# Responder to automatically set flash messages based on I18n API. It checks for
-
# message based on the current action, but also allows defaults to be set, using
-
# the following order:
-
#
-
# flash.controller_name.action_name.status
-
# flash.actions.action_name.status
-
#
-
# So, if you have a CarsController, create action, it will check for:
-
#
-
# flash.cars.create.status
-
# flash.actions.create.status
-
#
-
# The statuses by default are :notice (when the object can be created, updated
-
# or destroyed with success) and :alert (when the object cannot be created
-
# or updated).
-
#
-
# On I18n, the resource_name given is available as interpolation option,
-
# this means you can set:
-
#
-
# flash:
-
# actions:
-
# create:
-
# notice: "Hooray! %{resource_name} was successfully created!"
-
#
-
# But sometimes, flash messages are not that simple. Going back
-
# to cars example, you might want to say the brand of the car when it's
-
# updated. Well, that's easy also:
-
#
-
# flash:
-
# cars:
-
# update:
-
# notice: "Hooray! You just tuned your %{car_brand}!"
-
#
-
# Since :car_name is not available for interpolation by default, you have
-
# to overwrite interpolation_options in your controller.
-
#
-
# def interpolation_options
-
# { :car_brand => @car.brand }
-
# end
-
#
-
# Then you will finally have:
-
#
-
# 'Hooray! You just tuned your Aston Martin!'
-
#
-
# If your controller is namespaced, for example Admin::CarsController,
-
# the messages will be checked in the following order:
-
#
-
# flash.admin.cars.create.status
-
# flash.admin.actions.create.status
-
# flash.cars.create.status
-
# flash.actions.create.status
-
#
-
# You can also have flash messages with embedded HTML. Just create a scope that
-
# ends with <tt>_html</tt> as the scopes below:
-
#
-
# flash.actions.create.notice_html
-
# flash.cars.create.notice_html
-
#
-
# == Options
-
#
-
# FlashResponder also accepts some options through respond_with API.
-
#
-
# * :flash - When set to false, no flash message is set.
-
#
-
# respond_with(@user, :flash => true)
-
#
-
# * :notice - Supply the message to be set if the record has no errors.
-
# * :alert - Supply the message to be set if the record has errors.
-
#
-
# respond_with(@user, :notice => "Hooray! Welcome!", :alert => "Woot! You failed.")
-
#
-
# * :flash_now - Sets the flash message using flash.now. Accepts true, :on_failure or :on_sucess.
-
#
-
# == Configure status keys
-
#
-
# As said previously, FlashResponder by default use :notice and :alert
-
# keys. You can change that by setting the status_keys:
-
#
-
# Responders::FlashResponder.flash_keys = [ :success, :failure ]
-
#
-
# However, the options :notice and :alert to respond_with are kept :notice
-
# and :alert.
-
#
-
2
module FlashResponder
-
2
class << self
-
2
attr_accessor :flash_keys, :namespace_lookup, :helper
-
end
-
-
2
self.flash_keys = [ :notice, :alert ]
-
2
self.namespace_lookup = false
-
2
self.helper = Object.new.extend(
-
ActionView::Helpers::TranslationHelper,
-
ActionView::Helpers::TagHelper
-
)
-
-
2
def initialize(controller, resources, options={})
-
super
-
@flash = options.delete(:flash)
-
@notice = options.delete(:notice)
-
@alert = options.delete(:alert)
-
@flash_now = options.delete(:flash_now) { :on_failure }
-
end
-
-
2
def to_html
-
set_flash_message! if set_flash_message?
-
super
-
end
-
-
2
def to_js
-
set_flash_message! if set_flash_message?
-
defined?(super) ? super : to_format
-
end
-
-
2
protected
-
-
2
def set_flash_message!
-
if has_errors?
-
status = Responders::FlashResponder.flash_keys.last
-
set_flash(status, @alert)
-
else
-
status = Responders::FlashResponder.flash_keys.first
-
set_flash(status, @notice)
-
end
-
-
return if controller.flash[status].present?
-
-
options = mount_i18n_options(status)
-
message = Responders::FlashResponder.helper.t options[:default].shift, options
-
set_flash(status, message)
-
end
-
-
2
def set_flash(key, value)
-
return if value.blank?
-
flash = controller.flash
-
flash = flash.now if set_flash_now?
-
flash[key] ||= value
-
end
-
-
2
def set_flash_now?
-
@flash_now == true || format == :js ||
-
(default_action && (has_errors? ? @flash_now == :on_failure : @flash_now == :on_success))
-
end
-
-
2
def set_flash_message? #:nodoc:
-
!get? && @flash != false
-
end
-
-
2
def mount_i18n_options(status) #:nodoc:
-
resource_name = if resource.class.respond_to?(:model_name)
-
resource.class.model_name.human
-
else
-
resource.class.name.underscore.humanize
-
end
-
-
options = {
-
:default => flash_defaults_by_namespace(status),
-
:resource_name => resource_name,
-
:downcase_resource_name => resource_name.downcase
-
}
-
-
if controller.respond_to?(:interpolation_options, true)
-
options.merge!(controller.send(:interpolation_options))
-
end
-
-
options
-
end
-
-
2
def flash_defaults_by_namespace(status) #:nodoc:
-
defaults = []
-
slices = controller.controller_path.split('/')
-
lookup = Responders::FlashResponder.namespace_lookup
-
-
begin
-
controller_scope = :"flash.#{slices.fill(controller.controller_name, -1).join('.')}.#{controller.action_name}.#{status}"
-
-
actions_scope = lookup ? slices.fill('actions', -1).join('.') : :actions
-
actions_scope = :"flash.#{actions_scope}.#{controller.action_name}.#{status}"
-
-
defaults << :"#{controller_scope}_html"
-
defaults << controller_scope
-
-
defaults << :"#{actions_scope}_html"
-
defaults << actions_scope
-
-
slices.shift
-
end while slices.size > 0 && lookup
-
-
defaults << ""
-
end
-
end
-
end
-
1
require 'coffee_script'
-
-
1
module Sprockets
-
1
module Autoload
-
1
CoffeeScript = ::CoffeeScript
-
end
-
end
-
1
require 'sass'
-
-
1
module Sprockets
-
1
module Autoload
-
1
Sass = ::Sass
-
end
-
end
-
2
require 'fileutils'
-
2
require 'logger'
-
2
require 'sprockets/encoding_utils'
-
2
require 'sprockets/path_utils'
-
2
require 'zlib'
-
-
2
module Sprockets
-
2
class Cache
-
# Public: A file system cache store that automatically cleans up old keys.
-
#
-
# Assign the instance to the Environment#cache.
-
#
-
# environment.cache = Sprockets::Cache::FileStore.new("/tmp")
-
#
-
# See Also
-
#
-
# ActiveSupport::Cache::FileStore
-
#
-
2
class FileStore
-
# Internal: Default key limit for store.
-
2
DEFAULT_MAX_SIZE = 25 * 1024 * 1024
-
-
# Internal: Default standard error fatal logger.
-
#
-
# Returns a Logger.
-
2
def self.default_logger
-
logger = Logger.new($stderr)
-
logger.level = Logger::FATAL
-
logger
-
end
-
-
# Public: Initialize the cache store.
-
#
-
# root - A String path to a directory to persist cached values to.
-
# max_size - A Integer of the maximum number of keys the store will hold.
-
# (default: 1000).
-
2
def initialize(root, max_size = DEFAULT_MAX_SIZE, logger = self.class.default_logger)
-
2
@root = root
-
2552
@size = find_caches.inject(0) { |n, (_, stat)| n + stat.size }
-
2
@max_size = max_size
-
2
@gc_size = max_size * 0.75
-
2
@logger = logger
-
end
-
-
# Public: Retrieve value from cache.
-
#
-
# This API should not be used directly, but via the Cache wrapper API.
-
#
-
# key - String cache key.
-
#
-
# Returns Object or nil or the value is not set.
-
2
def get(key)
-
132
path = File.join(@root, "#{key}.cache")
-
-
132
value = safe_open(path) do |f|
-
132
begin
-
132
EncodingUtils.unmarshaled_deflated(f.read, Zlib::MAX_WBITS)
-
rescue Exception => e
-
@logger.error do
-
"#{self.class}[#{path}] could not be unmarshaled: " +
-
"#{e.class}: #{e.message}"
-
end
-
nil
-
end
-
end
-
-
132
if value
-
132
FileUtils.touch(path)
-
132
value
-
end
-
end
-
-
# Public: Set a key and value in the cache.
-
#
-
# This API should not be used directly, but via the Cache wrapper API.
-
#
-
# key - String cache key.
-
# value - Object value.
-
#
-
# Returns Object value.
-
2
def set(key, value)
-
path = File.join(@root, "#{key}.cache")
-
-
# Ensure directory exists
-
FileUtils.mkdir_p File.dirname(path)
-
-
# Check if cache exists before writing
-
exists = File.exist?(path)
-
-
# Serialize value
-
marshaled = Marshal.dump(value)
-
-
# Compress if larger than 4KB
-
if marshaled.bytesize > 4 * 1024
-
deflater = Zlib::Deflate.new(
-
Zlib::BEST_COMPRESSION,
-
Zlib::MAX_WBITS,
-
Zlib::MAX_MEM_LEVEL,
-
Zlib::DEFAULT_STRATEGY
-
)
-
deflater << marshaled
-
raw = deflater.finish
-
else
-
raw = marshaled
-
end
-
-
# Write data
-
PathUtils.atomic_write(path) do |f|
-
f.write(raw)
-
@size += f.size unless exists
-
end
-
-
# GC if necessary
-
gc! if @size > @max_size
-
-
value
-
end
-
-
# Public: Pretty inspect
-
#
-
# Returns String.
-
2
def inspect
-
"#<#{self.class} size=#{@size}/#{@max_size}>"
-
end
-
-
2
private
-
# Internal: Get all cache files along with stats.
-
#
-
# Returns an Array of [String filename, File::Stat] pairs sorted by
-
# mtime.
-
2
def find_caches
-
Dir.glob(File.join(@root, '**/*.cache')).reduce([]) { |stats, filename|
-
2550
stat = safe_stat(filename)
-
# stat maybe nil if file was removed between the time we called
-
# dir.glob and the next stat
-
2550
stats << [filename, stat] if stat
-
2550
stats
-
2552
}.sort_by { |_, stat| stat.mtime.to_i }
-
end
-
-
2
def compute_size(caches)
-
caches.inject(0) { |sum, (_, stat)| sum + stat.size }
-
end
-
-
2
def safe_stat(fn)
-
2550
File.stat(fn)
-
rescue Errno::ENOENT
-
nil
-
end
-
-
2
def safe_open(path, &block)
-
132
if File.exist?(path)
-
132
File.open(path, 'rb', &block)
-
end
-
rescue Errno::ENOENT
-
end
-
-
2
def gc!
-
start_time = Time.now
-
-
caches = find_caches
-
size = compute_size(caches)
-
-
delete_caches, keep_caches = caches.partition { |filename, stat|
-
deleted = size > @gc_size
-
size -= stat.size
-
deleted
-
}
-
-
return if delete_caches.empty?
-
-
FileUtils.remove(delete_caches.map(&:first), force: true)
-
@size = compute_size(keep_caches)
-
-
@logger.warn do
-
secs = Time.now.to_f - start_time.to_f
-
"#{self.class}[#{@root}] garbage collected " +
-
"#{delete_caches.size} files (#{(secs * 1000).to_i}ms)"
-
end
-
end
-
end
-
end
-
end
-
2
require "thor/command"
-
2
require "thor/core_ext/hash_with_indifferent_access"
-
2
require "thor/core_ext/ordered_hash"
-
2
require "thor/error"
-
2
require "thor/invocation"
-
2
require "thor/parser"
-
2
require "thor/shell"
-
2
require "thor/line_editor"
-
2
require "thor/util"
-
-
2
class Thor
-
2
autoload :Actions, "thor/actions"
-
2
autoload :RakeCompat, "thor/rake_compat"
-
2
autoload :Group, "thor/group"
-
-
# Shortcuts for help.
-
2
HELP_MAPPINGS = %w[-h -? --help -D]
-
-
# Thor methods that should not be overwritten by the user.
-
2
THOR_RESERVED_WORDS = %w[invoke shell options behavior root destination_root relative_root
-
action add_file create_file in_root inside run run_ruby_script]
-
-
2
TEMPLATE_EXTNAME = ".tt"
-
-
2
module Base
-
2
attr_accessor :options, :parent_options, :args
-
-
# It receives arguments in an Array and two hashes, one for options and
-
# other for configuration.
-
#
-
# Notice that it does not check if all required arguments were supplied.
-
# It should be done by the parser.
-
#
-
# ==== Parameters
-
# args<Array[Object]>:: An array of objects. The objects are applied to their
-
# respective accessors declared with <tt>argument</tt>.
-
#
-
# options<Hash>:: An options hash that will be available as self.options.
-
# The hash given is converted to a hash with indifferent
-
# access, magic predicates (options.skip?) and then frozen.
-
#
-
# config<Hash>:: Configuration for this Thor class.
-
#
-
2
def initialize(args = [], local_options = {}, config = {}) # rubocop:disable MethodLength
-
parse_options = self.class.class_options
-
-
# The start method splits inbound arguments at the first argument
-
# that looks like an option (starts with - or --). It then calls
-
# new, passing in the two halves of the arguments Array as the
-
# first two parameters.
-
-
command_options = config.delete(:command_options) # hook for start
-
parse_options = parse_options.merge(command_options) if command_options
-
if local_options.is_a?(Array)
-
array_options, hash_options = local_options, {}
-
else
-
# Handle the case where the class was explicitly instantiated
-
# with pre-parsed options.
-
array_options, hash_options = [], local_options
-
end
-
-
# Let Thor::Options parse the options first, so it can remove
-
# declared options from the array. This will leave us with
-
# a list of arguments that weren't declared.
-
stop_on_unknown = self.class.stop_on_unknown_option? config[:current_command]
-
opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown)
-
self.options = opts.parse(array_options)
-
self.options = config[:class_options].merge(options) if config[:class_options]
-
-
# If unknown options are disallowed, make sure that none of the
-
# remaining arguments looks like an option.
-
opts.check_unknown! if self.class.check_unknown_options?(config)
-
-
# Add the remaining arguments from the options parser to the
-
# arguments passed in to initialize. Then remove any positional
-
# arguments declared using #argument (this is primarily used
-
# by Thor::Group). Tis will leave us with the remaining
-
# positional arguments.
-
to_parse = args
-
to_parse += opts.remaining unless self.class.strict_args_position?(config)
-
-
thor_args = Thor::Arguments.new(self.class.arguments)
-
thor_args.parse(to_parse).each { |k, v| __send__("#{k}=", v) }
-
@args = thor_args.remaining
-
end
-
-
2
class << self
-
2
def included(base) #:nodoc:
-
2
base.extend ClassMethods
-
2
base.send :include, Invocation
-
2
base.send :include, Shell
-
end
-
-
# Returns the classes that inherits from Thor or Thor::Group.
-
#
-
# ==== Returns
-
# Array[Class]
-
#
-
2
def subclasses
-
@subclasses ||= []
-
end
-
-
# Returns the files where the subclasses are kept.
-
#
-
# ==== Returns
-
# Hash[path<String> => Class]
-
#
-
2
def subclass_files
-
@subclass_files ||= Hash.new { |h, k| h[k] = [] }
-
end
-
-
# Whenever a class inherits from Thor or Thor::Group, we should track the
-
# class and the file on Thor::Base. This is the method responsable for it.
-
#
-
2
def register_klass_file(klass) #:nodoc:
-
file = caller[1].match(/(.*):\d+/)[1]
-
Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass)
-
-
file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
-
file_subclasses << klass unless file_subclasses.include?(klass)
-
end
-
end
-
-
2
module ClassMethods
-
2
def attr_reader(*) #:nodoc:
-
no_commands { super }
-
end
-
-
2
def attr_writer(*) #:nodoc:
-
no_commands { super }
-
end
-
-
2
def attr_accessor(*) #:nodoc:
-
no_commands { super }
-
end
-
-
# If you want to raise an error for unknown options, call check_unknown_options!
-
# This is disabled by default to allow dynamic invocations.
-
2
def check_unknown_options!
-
@check_unknown_options = true
-
end
-
-
2
def check_unknown_options #:nodoc:
-
@check_unknown_options ||= from_superclass(:check_unknown_options, false)
-
end
-
-
2
def check_unknown_options?(config) #:nodoc:
-
!!check_unknown_options
-
end
-
-
# If true, option parsing is suspended as soon as an unknown option or a
-
# regular argument is encountered. All remaining arguments are passed to
-
# the command as regular arguments.
-
2
def stop_on_unknown_option?(command_name) #:nodoc:
-
false
-
end
-
-
# If you want only strict string args (useful when cascading thor classes),
-
# call strict_args_position! This is disabled by default to allow dynamic
-
# invocations.
-
2
def strict_args_position!
-
@strict_args_position = true
-
end
-
-
2
def strict_args_position #:nodoc:
-
@strict_args_position ||= from_superclass(:strict_args_position, false)
-
end
-
-
2
def strict_args_position?(config) #:nodoc:
-
!!strict_args_position
-
end
-
-
# Adds an argument to the class and creates an attr_accessor for it.
-
#
-
# Arguments are different from options in several aspects. The first one
-
# is how they are parsed from the command line, arguments are retrieved
-
# from position:
-
#
-
# thor command NAME
-
#
-
# Instead of:
-
#
-
# thor command --name=NAME
-
#
-
# Besides, arguments are used inside your code as an accessor (self.argument),
-
# while options are all kept in a hash (self.options).
-
#
-
# Finally, arguments cannot have type :default or :boolean but can be
-
# optional (supplying :optional => :true or :required => false), although
-
# you cannot have a required argument after a non-required argument. If you
-
# try it, an error is raised.
-
#
-
# ==== Parameters
-
# name<Symbol>:: The name of the argument.
-
# options<Hash>:: Described below.
-
#
-
# ==== Options
-
# :desc - Description for the argument.
-
# :required - If the argument is required or not.
-
# :optional - If the argument is optional or not.
-
# :type - The type of the argument, can be :string, :hash, :array, :numeric.
-
# :default - Default value for this argument. It cannot be required and have default values.
-
# :banner - String to show on usage notes.
-
#
-
# ==== Errors
-
# ArgumentError:: Raised if you supply a required argument after a non required one.
-
#
-
2
def argument(name, options = {}) # rubocop:disable MethodLength
-
is_thor_reserved_word?(name, :argument)
-
no_commands { attr_accessor name }
-
-
required = if options.key?(:optional)
-
!options[:optional]
-
elsif options.key?(:required)
-
options[:required]
-
else
-
options[:default].nil?
-
end
-
-
remove_argument name
-
-
arguments.each do |argument|
-
next if argument.required?
-
fail ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " <<
-
"the non-required argument #{argument.human_name.inspect}."
-
end if required
-
-
options[:required] = required
-
-
arguments << Thor::Argument.new(name, options)
-
end
-
-
# Returns this class arguments, looking up in the ancestors chain.
-
#
-
# ==== Returns
-
# Array[Thor::Argument]
-
#
-
2
def arguments
-
@arguments ||= from_superclass(:arguments, [])
-
end
-
-
# Adds a bunch of options to the set of class options.
-
#
-
# class_options :foo => false, :bar => :required, :baz => :string
-
#
-
# If you prefer more detailed declaration, check class_option.
-
#
-
# ==== Parameters
-
# Hash[Symbol => Object]
-
#
-
2
def class_options(options = nil)
-
@class_options ||= from_superclass(:class_options, {})
-
build_options(options, @class_options) if options
-
@class_options
-
end
-
-
# Adds an option to the set of class options
-
#
-
# ==== Parameters
-
# name<Symbol>:: The name of the argument.
-
# options<Hash>:: Described below.
-
#
-
# ==== Options
-
# :desc:: -- Description for the argument.
-
# :required:: -- If the argument is required or not.
-
# :default:: -- Default value for this argument.
-
# :group:: -- The group for this options. Use by class options to output options in different levels.
-
# :aliases:: -- Aliases for this option. <b>Note:</b> Thor follows a convention of one-dash-one-letter options. Thus aliases like "-something" wouldn't be parsed; use either "\--something" or "-s" instead.
-
# :type:: -- The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
-
# :banner:: -- String to show on usage notes.
-
# :hide:: -- If you want to hide this option from the help.
-
#
-
2
def class_option(name, options = {})
-
build_option(name, options, class_options)
-
end
-
-
# Removes a previous defined argument. If :undefine is given, undefine
-
# accessors as well.
-
#
-
# ==== Parameters
-
# names<Array>:: Arguments to be removed
-
#
-
# ==== Examples
-
#
-
# remove_argument :foo
-
# remove_argument :foo, :bar, :baz, :undefine => true
-
#
-
2
def remove_argument(*names)
-
options = names.last.is_a?(Hash) ? names.pop : {}
-
-
names.each do |name|
-
arguments.delete_if { |a| a.name == name.to_s }
-
undef_method name, "#{name}=" if options[:undefine]
-
end
-
end
-
-
# Removes a previous defined class option.
-
#
-
# ==== Parameters
-
# names<Array>:: Class options to be removed
-
#
-
# ==== Examples
-
#
-
# remove_class_option :foo
-
# remove_class_option :foo, :bar, :baz
-
#
-
2
def remove_class_option(*names)
-
names.each do |name|
-
class_options.delete(name)
-
end
-
end
-
-
# Defines the group. This is used when thor list is invoked so you can specify
-
# that only commands from a pre-defined group will be shown. Defaults to standard.
-
#
-
# ==== Parameters
-
# name<String|Symbol>
-
#
-
2
def group(name = nil)
-
if name
-
@group = name.to_s
-
else
-
@group ||= from_superclass(:group, "standard")
-
end
-
end
-
-
# Returns the commands for this Thor class.
-
#
-
# ==== Returns
-
# OrderedHash:: An ordered hash with commands names as keys and Thor::Command
-
# objects as values.
-
#
-
2
def commands
-
@commands ||= Thor::CoreExt::OrderedHash.new
-
end
-
2
alias_method :tasks, :commands
-
-
# Returns the commands for this Thor class and all subclasses.
-
#
-
# ==== Returns
-
# OrderedHash:: An ordered hash with commands names as keys and Thor::Command
-
# objects as values.
-
#
-
2
def all_commands
-
@all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new)
-
@all_commands.merge(commands)
-
end
-
2
alias_method :all_tasks, :all_commands
-
-
# Removes a given command from this Thor class. This is usually done if you
-
# are inheriting from another class and don't want it to be available
-
# anymore.
-
#
-
# By default it only remove the mapping to the command. But you can supply
-
# :undefine => true to undefine the method from the class as well.
-
#
-
# ==== Parameters
-
# name<Symbol|String>:: The name of the command to be removed
-
# options<Hash>:: You can give :undefine => true if you want commands the method
-
# to be undefined from the class as well.
-
#
-
2
def remove_command(*names)
-
options = names.last.is_a?(Hash) ? names.pop : {}
-
-
names.each do |name|
-
commands.delete(name.to_s)
-
all_commands.delete(name.to_s)
-
undef_method name if options[:undefine]
-
end
-
end
-
2
alias_method :remove_task, :remove_command
-
-
# All methods defined inside the given block are not added as commands.
-
#
-
# So you can do:
-
#
-
# class MyScript < Thor
-
# no_commands do
-
# def this_is_not_a_command
-
# end
-
# end
-
# end
-
#
-
# You can also add the method and remove it from the command list:
-
#
-
# class MyScript < Thor
-
# def this_is_not_a_command
-
# end
-
# remove_command :this_is_not_a_command
-
# end
-
#
-
2
def no_commands
-
@no_commands = true
-
yield
-
ensure
-
@no_commands = false
-
end
-
2
alias_method :no_tasks, :no_commands
-
-
# Sets the namespace for the Thor or Thor::Group class. By default the
-
# namespace is retrieved from the class name. If your Thor class is named
-
# Scripts::MyScript, the help method, for example, will be called as:
-
#
-
# thor scripts:my_script -h
-
#
-
# If you change the namespace:
-
#
-
# namespace :my_scripts
-
#
-
# You change how your commands are invoked:
-
#
-
# thor my_scripts -h
-
#
-
# Finally, if you change your namespace to default:
-
#
-
# namespace :default
-
#
-
# Your commands can be invoked with a shortcut. Instead of:
-
#
-
# thor :my_command
-
#
-
2
def namespace(name = nil)
-
if name
-
@namespace = name.to_s
-
else
-
@namespace ||= Thor::Util.namespace_from_thor_class(self)
-
end
-
end
-
-
# Parses the command and options from the given args, instantiate the class
-
# and invoke the command. This method is used when the arguments must be parsed
-
# from an array. If you are inside Ruby and want to use a Thor class, you
-
# can simply initialize it:
-
#
-
# script = MyScript.new(args, options, config)
-
# script.invoke(:command, first_arg, second_arg, third_arg)
-
#
-
2
def start(given_args = ARGV, config = {})
-
config[:shell] ||= Thor::Base.shell.new
-
dispatch(nil, given_args.dup, nil, config)
-
rescue Thor::Error => e
-
config[:debug] || ENV["THOR_DEBUG"] == "1" ? (raise e) : config[:shell].error(e.message)
-
exit(1) if exit_on_failure?
-
rescue Errno::EPIPE
-
# This happens if a thor command is piped to something like `head`,
-
# which closes the pipe when it's done reading. This will also
-
# mean that if the pipe is closed, further unnecessary
-
# computation will not occur.
-
exit(0)
-
end
-
-
# Allows to use private methods from parent in child classes as commands.
-
#
-
# ==== Parameters
-
# names<Array>:: Method names to be used as commands
-
#
-
# ==== Examples
-
#
-
# public_command :foo
-
# public_command :foo, :bar, :baz
-
#
-
2
def public_command(*names)
-
names.each do |name|
-
class_eval "def #{name}(*); super end"
-
end
-
end
-
2
alias_method :public_task, :public_command
-
-
2
def handle_no_command_error(command, has_namespace = $thor_runner) #:nodoc:
-
if has_namespace
-
fail UndefinedCommandError, "Could not find command #{command.inspect} in #{namespace.inspect} namespace."
-
else
-
fail UndefinedCommandError, "Could not find command #{command.inspect}."
-
end
-
end
-
2
alias_method :handle_no_task_error, :handle_no_command_error
-
-
2
def handle_argument_error(command, error, args, arity) #:nodoc:
-
msg = "ERROR: \"#{basename} #{command.name}\" was called with "
-
msg << "no arguments" if args.empty?
-
msg << "arguments " << args.inspect unless args.empty?
-
msg << "\nUsage: #{banner(command).inspect}"
-
fail InvocationError, msg
-
end
-
-
2
protected
-
-
# Prints the class options per group. If an option does not belong to
-
# any group, it's printed as Class option.
-
#
-
2
def class_options_help(shell, groups = {}) #:nodoc:
-
# Group options by group
-
class_options.each do |_, value|
-
groups[value.group] ||= []
-
groups[value.group] << value
-
end
-
-
# Deal with default group
-
global_options = groups.delete(nil) || []
-
print_options(shell, global_options)
-
-
# Print all others
-
groups.each do |group_name, options|
-
print_options(shell, options, group_name)
-
end
-
end
-
-
# Receives a set of options and print them.
-
2
def print_options(shell, options, group_name = nil)
-
return if options.empty?
-
-
list = []
-
padding = options.map { |o| o.aliases.size }.max.to_i * 4
-
-
options.each do |option|
-
unless option.hide
-
item = [option.usage(padding)]
-
item.push(option.description ? "# #{option.description}" : "")
-
-
list << item
-
list << ["", "# Default: #{option.default}"] if option.show_default?
-
list << ["", "# Possible values: #{option.enum.join(', ')}"] if option.enum
-
end
-
end
-
-
shell.say(group_name ? "#{group_name} options:" : "Options:")
-
shell.print_table(list, :indent => 2)
-
shell.say ""
-
end
-
-
# Raises an error if the word given is a Thor reserved word.
-
2
def is_thor_reserved_word?(word, type) #:nodoc:
-
return false unless THOR_RESERVED_WORDS.include?(word.to_s)
-
fail "#{word.inspect} is a Thor reserved word and cannot be defined as #{type}"
-
end
-
-
# Build an option and adds it to the given scope.
-
#
-
# ==== Parameters
-
# name<Symbol>:: The name of the argument.
-
# options<Hash>:: Described in both class_option and method_option.
-
# scope<Hash>:: Options hash that is being built up
-
2
def build_option(name, options, scope) #:nodoc:
-
scope[name] = Thor::Option.new(name, options)
-
end
-
-
# Receives a hash of options, parse them and add to the scope. This is a
-
# fast way to set a bunch of options:
-
#
-
# build_options :foo => true, :bar => :required, :baz => :string
-
#
-
# ==== Parameters
-
# Hash[Symbol => Object]
-
2
def build_options(options, scope) #:nodoc:
-
options.each do |key, value|
-
scope[key] = Thor::Option.parse(key, value)
-
end
-
end
-
-
# Finds a command with the given name. If the command belongs to the current
-
# class, just return it, otherwise dup it and add the fresh copy to the
-
# current command hash.
-
2
def find_and_refresh_command(name) #:nodoc:
-
if commands[name.to_s]
-
commands[name.to_s]
-
elsif command = all_commands[name.to_s] # rubocop:disable AssignmentInCondition
-
commands[name.to_s] = command.clone
-
else
-
fail ArgumentError, "You supplied :for => #{name.inspect}, but the command #{name.inspect} could not be found."
-
end
-
end
-
2
alias_method :find_and_refresh_task, :find_and_refresh_command
-
-
# Everytime someone inherits from a Thor class, register the klass
-
# and file into baseclass.
-
2
def inherited(klass)
-
Thor::Base.register_klass_file(klass)
-
klass.instance_variable_set(:@no_commands, false)
-
end
-
-
# Fire this callback whenever a method is added. Added methods are
-
# tracked as commands by invoking the create_command method.
-
2
def method_added(meth)
-
2
meth = meth.to_s
-
-
2
if meth == "initialize"
-
initialize_added
-
return
-
end
-
-
# Return if it's not a public instance method
-
2
return unless public_method_defined?(meth.to_sym)
-
-
@no_commands ||= false
-
return if @no_commands || !create_command(meth)
-
-
is_thor_reserved_word?(meth, :command)
-
Thor::Base.register_klass_file(self)
-
end
-
-
# Retrieves a value from superclass. If it reaches the baseclass,
-
# returns default.
-
2
def from_superclass(method, default = nil)
-
if self == baseclass || !superclass.respond_to?(method, true)
-
default
-
else
-
value = superclass.send(method)
-
-
# Ruby implements `dup` on Object, but raises a `TypeError`
-
# if the method is called on immediates. As a result, we
-
# don't have a good way to check whether dup will succeed
-
# without calling it and rescuing the TypeError.
-
begin
-
value.dup
-
rescue TypeError
-
value
-
end
-
-
end
-
end
-
-
# A flag that makes the process exit with status 1 if any error happens.
-
2
def exit_on_failure?
-
false
-
end
-
-
#
-
# The basename of the program invoking the thor class.
-
#
-
2
def basename
-
File.basename($PROGRAM_NAME).split(" ").first
-
end
-
-
# SIGNATURE: Sets the baseclass. This is where the superclass lookup
-
# finishes.
-
2
def baseclass #:nodoc:
-
end
-
-
# SIGNATURE: Creates a new command if valid_command? is true. This method is
-
# called when a new method is added to the class.
-
2
def create_command(meth) #:nodoc:
-
end
-
2
alias_method :create_task, :create_command
-
-
# SIGNATURE: Defines behavior when the initialize method is added to the
-
# class.
-
2
def initialize_added #:nodoc:
-
end
-
-
# SIGNATURE: The hook invoked by start.
-
2
def dispatch(command, given_args, given_opts, config) #:nodoc:
-
fail NotImplementedError
-
end
-
end
-
end
-
end
-
2
class Thor
-
2
class Command < Struct.new(:name, :description, :long_description, :usage, :options)
-
2
FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
-
-
2
def initialize(name, description, long_description, usage, options = nil)
-
super(name.to_s, description, long_description, usage, options || {})
-
end
-
-
2
def initialize_copy(other) #:nodoc:
-
super(other)
-
self.options = other.options.dup if other.options
-
end
-
-
2
def hidden?
-
false
-
end
-
-
# By default, a command invokes a method in the thor class. You can change this
-
# implementation to create custom commands.
-
2
def run(instance, args = [])
-
arity = nil
-
-
if private_method?(instance)
-
instance.class.handle_no_command_error(name)
-
elsif public_method?(instance)
-
arity = instance.method(name).arity
-
instance.__send__(name, *args)
-
elsif local_method?(instance, :method_missing)
-
instance.__send__(:method_missing, name.to_sym, *args)
-
else
-
instance.class.handle_no_command_error(name)
-
end
-
rescue ArgumentError => e
-
handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e)
-
rescue NoMethodError => e
-
handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (fail e)
-
end
-
-
# Returns the formatted usage by injecting given required arguments
-
# and required options into the given usage.
-
2
def formatted_usage(klass, namespace = true, subcommand = false)
-
if namespace
-
namespace = klass.namespace
-
formatted = "#{namespace.gsub(/^(default)/, '')}:"
-
end
-
formatted = "#{klass.namespace.split(':').last} " if subcommand
-
-
formatted ||= ""
-
-
# Add usage with required arguments
-
formatted << if klass && !klass.arguments.empty?
-
usage.to_s.gsub(/^#{name}/) do |match|
-
match << " " << klass.arguments.map { |a| a.usage }.compact.join(" ")
-
end
-
else
-
usage.to_s
-
end
-
-
# Add required options
-
formatted << " #{required_options}"
-
-
# Strip and go!
-
formatted.strip
-
end
-
-
2
protected
-
-
2
def not_debugging?(instance)
-
!(instance.class.respond_to?(:debugging) && instance.class.debugging)
-
end
-
-
2
def required_options
-
@required_options ||= options.map { |_, o| o.usage if o.required? }.compact.sort.join(" ")
-
end
-
-
# Given a target, checks if this class name is a public method.
-
2
def public_method?(instance) #:nodoc:
-
!(instance.public_methods & [name.to_s, name.to_sym]).empty?
-
end
-
-
2
def private_method?(instance)
-
!(instance.private_methods & [name.to_s, name.to_sym]).empty?
-
end
-
-
2
def local_method?(instance, name)
-
methods = instance.public_methods(false) + instance.private_methods(false) + instance.protected_methods(false)
-
!(methods & [name.to_s, name.to_sym]).empty?
-
end
-
-
2
def sans_backtrace(backtrace, caller) #:nodoc:
-
saned = backtrace.reject { |frame| frame =~ FILE_REGEXP || (frame =~ /\.java:/ && RUBY_PLATFORM =~ /java/) || (frame =~ /^kernel\// && RUBY_ENGINE =~ /rbx/) }
-
saned - caller
-
end
-
-
2
def handle_argument_error?(instance, error, caller)
-
not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin
-
saned = sans_backtrace(error.backtrace, caller)
-
# Ruby 1.9 always include the called method in the backtrace
-
saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9")
-
end
-
end
-
-
2
def handle_no_method_error?(instance, error, caller)
-
not_debugging?(instance) &&
-
error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
-
end
-
end
-
2
Task = Command # rubocop:disable ConstantName
-
-
# A command that is hidden in help messages but still invocable.
-
2
class HiddenCommand < Command
-
2
def hidden?
-
true
-
end
-
end
-
2
HiddenTask = HiddenCommand # rubocop:disable ConstantName
-
-
# A dynamic command that handles method missing scenarios.
-
2
class DynamicCommand < Command
-
2
def initialize(name, options = nil)
-
super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options)
-
end
-
-
2
def run(instance, args = [])
-
if (instance.methods & [name.to_s, name.to_sym]).empty?
-
super
-
else
-
instance.class.handle_no_command_error(name)
-
end
-
end
-
end
-
2
DynamicTask = DynamicCommand # rubocop:disable ConstantName
-
end
-
2
class Thor
-
2
module CoreExt #:nodoc:
-
# A hash with indifferent access and magic predicates.
-
#
-
# hash = Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true
-
#
-
# hash[:foo] #=> 'bar'
-
# hash['foo'] #=> 'bar'
-
# hash.foo? #=> true
-
#
-
2
class HashWithIndifferentAccess < ::Hash #:nodoc:
-
2
def initialize(hash = {})
-
super()
-
hash.each do |key, value|
-
self[convert_key(key)] = value
-
end
-
end
-
-
2
def [](key)
-
super(convert_key(key))
-
end
-
-
2
def []=(key, value)
-
super(convert_key(key), value)
-
end
-
-
2
def delete(key)
-
super(convert_key(key))
-
end
-
-
2
def values_at(*indices)
-
indices.map { |key| self[convert_key(key)] }
-
end
-
-
2
def merge(other)
-
dup.merge!(other)
-
end
-
-
2
def merge!(other)
-
other.each do |key, value|
-
self[convert_key(key)] = value
-
end
-
self
-
end
-
-
# Convert to a Hash with String keys.
-
2
def to_hash
-
Hash.new(default).merge!(self)
-
end
-
-
2
protected
-
-
2
def convert_key(key)
-
key.is_a?(Symbol) ? key.to_s : key
-
end
-
-
# Magic predicates. For instance:
-
#
-
# options.force? # => !!options['force']
-
# options.shebang # => "/usr/lib/local/ruby"
-
# options.test_framework?(:rspec) # => options[:test_framework] == :rspec
-
#
-
2
def method_missing(method, *args, &block)
-
method = method.to_s
-
if method =~ /^(\w+)\?$/
-
if args.empty?
-
!!self[$1]
-
else
-
self[$1] == args.first
-
end
-
else
-
self[method]
-
end
-
end
-
end
-
end
-
end
-
2
class Thor
-
2
module CoreExt #:nodoc:
-
2
if RUBY_VERSION >= "1.9"
-
2
class OrderedHash < ::Hash
-
end
-
else
-
# This class is based on the Ruby 1.9 ordered hashes.
-
#
-
# It keeps the semantics and most of the efficiency of normal hashes
-
# while also keeping track of the order in which elements were set.
-
#
-
class OrderedHash #:nodoc:
-
include Enumerable
-
-
Node = Struct.new(:key, :value, :next, :prev)
-
-
def initialize
-
@hash = {}
-
end
-
-
def [](key)
-
@hash[key] && @hash[key].value
-
end
-
-
def []=(key, value)
-
if node = @hash[key] # rubocop:disable AssignmentInCondition
-
node.value = value
-
else
-
node = Node.new(key, value)
-
-
if !defined?(@first) || @first.nil?
-
@first = @last = node
-
else
-
node.prev = @last
-
@last.next = node
-
@last = node
-
end
-
end
-
-
@hash[key] = node
-
value
-
end
-
-
def delete(key)
-
if node = @hash[key] # rubocop:disable AssignmentInCondition
-
prev_node = node.prev
-
next_node = node.next
-
-
next_node.prev = prev_node if next_node
-
prev_node.next = next_node if prev_node
-
-
@first = next_node if @first == node
-
@last = prev_node if @last == node
-
-
value = node.value
-
end
-
-
@hash.delete(key)
-
value
-
end
-
-
def keys
-
map { |k, v| k }
-
end
-
-
def values
-
map { |k, v| v }
-
end
-
-
def each
-
return unless defined?(@first) && @first
-
yield [@first.key, @first.value]
-
node = @first
-
yield [node.key, node.value] while node = node.next # rubocop:disable AssignmentInCondition
-
self
-
end
-
-
def merge(other)
-
hash = self.class.new
-
-
each do |key, value|
-
hash[key] = value
-
end
-
-
other.each do |key, value|
-
hash[key] = value
-
end
-
-
hash
-
end
-
-
def empty?
-
@hash.empty?
-
end
-
end
-
end
-
end
-
end
-
2
class Thor
-
# Thor::Error is raised when it's caused by wrong usage of thor classes. Those
-
# errors have their backtrace suppressed and are nicely shown to the user.
-
#
-
# Errors that are caused by the developer, like declaring a method which
-
# overwrites a thor keyword, it SHOULD NOT raise a Thor::Error. This way, we
-
# ensure that developer errors are shown with full backtrace.
-
2
class Error < StandardError
-
end
-
-
# Raised when a command was not found.
-
2
class UndefinedCommandError < Error
-
end
-
2
UndefinedTaskError = UndefinedCommandError # rubocop:disable ConstantName
-
-
2
class AmbiguousCommandError < Error
-
end
-
2
AmbiguousTaskError = AmbiguousCommandError # rubocop:disable ConstantName
-
-
# Raised when a command was found, but not invoked properly.
-
2
class InvocationError < Error
-
end
-
-
2
class UnknownArgumentError < Error
-
end
-
-
2
class RequiredArgumentMissingError < InvocationError
-
end
-
-
2
class MalformattedArgumentError < InvocationError
-
end
-
end
-
2
require "thor/base"
-
-
# Thor has a special class called Thor::Group. The main difference to Thor class
-
# is that it invokes all commands at once. It also include some methods that allows
-
# invocations to be done at the class method, which are not available to Thor
-
# commands.
-
2
class Thor::Group # rubocop:disable ClassLength
-
2
class << self
-
# The description for this Thor::Group. If none is provided, but a source root
-
# exists, tries to find the USAGE one folder above it, otherwise searches
-
# in the superclass.
-
#
-
# ==== Parameters
-
# description<String>:: The description for this Thor::Group.
-
#
-
2
def desc(description = nil)
-
if description
-
@desc = description
-
else
-
@desc ||= from_superclass(:desc, nil)
-
end
-
end
-
-
# Prints help information.
-
#
-
# ==== Options
-
# short:: When true, shows only usage.
-
#
-
2
def help(shell)
-
shell.say "Usage:"
-
shell.say " #{banner}\n"
-
shell.say
-
class_options_help(shell)
-
shell.say desc if desc
-
end
-
-
# Stores invocations for this class merging with superclass values.
-
#
-
2
def invocations #:nodoc:
-
@invocations ||= from_superclass(:invocations, {})
-
end
-
-
# Stores invocation blocks used on invoke_from_option.
-
#
-
2
def invocation_blocks #:nodoc:
-
@invocation_blocks ||= from_superclass(:invocation_blocks, {})
-
end
-
-
# Invoke the given namespace or class given. It adds an instance
-
# method that will invoke the klass and command. You can give a block to
-
# configure how it will be invoked.
-
#
-
# The namespace/class given will have its options showed on the help
-
# usage. Check invoke_from_option for more information.
-
#
-
2
def invoke(*names, &block) # rubocop:disable MethodLength
-
options = names.last.is_a?(Hash) ? names.pop : {}
-
verbose = options.fetch(:verbose, true)
-
-
names.each do |name|
-
invocations[name] = false
-
invocation_blocks[name] = block if block_given?
-
-
class_eval <<-METHOD, __FILE__, __LINE__
-
def _invoke_#{name.to_s.gsub(/\W/, "_")}
-
klass, command = self.class.prepare_for_invocation(nil, #{name.inspect})
-
-
if klass
-
say_status :invoke, #{name.inspect}, #{verbose.inspect}
-
block = self.class.invocation_blocks[#{name.inspect}]
-
_invoke_for_class_method klass, command, &block
-
else
-
say_status :error, %(#{name.inspect} [not found]), :red
-
end
-
end
-
METHOD
-
end
-
end
-
-
# Invoke a thor class based on the value supplied by the user to the
-
# given option named "name". A class option must be created before this
-
# method is invoked for each name given.
-
#
-
# ==== Examples
-
#
-
# class GemGenerator < Thor::Group
-
# class_option :test_framework, :type => :string
-
# invoke_from_option :test_framework
-
# end
-
#
-
# ==== Boolean options
-
#
-
# In some cases, you want to invoke a thor class if some option is true or
-
# false. This is automatically handled by invoke_from_option. Then the
-
# option name is used to invoke the generator.
-
#
-
# ==== Preparing for invocation
-
#
-
# In some cases you want to customize how a specified hook is going to be
-
# invoked. You can do that by overwriting the class method
-
# prepare_for_invocation. The class method must necessarily return a klass
-
# and an optional command.
-
#
-
# ==== Custom invocations
-
#
-
# You can also supply a block to customize how the option is going to be
-
# invoked. The block receives two parameters, an instance of the current
-
# class and the klass to be invoked.
-
#
-
2
def invoke_from_option(*names, &block) # rubocop:disable MethodLength
-
options = names.last.is_a?(Hash) ? names.pop : {}
-
verbose = options.fetch(:verbose, :white)
-
-
names.each do |name|
-
unless class_options.key?(name)
-
fail ArgumentError, "You have to define the option #{name.inspect} " <<
-
"before setting invoke_from_option."
-
end
-
-
invocations[name] = true
-
invocation_blocks[name] = block if block_given?
-
-
class_eval <<-METHOD, __FILE__, __LINE__
-
def _invoke_from_option_#{name.to_s.gsub(/\W/, "_")}
-
return unless options[#{name.inspect}]
-
-
value = options[#{name.inspect}]
-
value = #{name.inspect} if TrueClass === value
-
klass, command = self.class.prepare_for_invocation(#{name.inspect}, value)
-
-
if klass
-
say_status :invoke, value, #{verbose.inspect}
-
block = self.class.invocation_blocks[#{name.inspect}]
-
_invoke_for_class_method klass, command, &block
-
else
-
say_status :error, %(\#{value} [not found]), :red
-
end
-
end
-
METHOD
-
end
-
end
-
-
# Remove a previously added invocation.
-
#
-
# ==== Examples
-
#
-
# remove_invocation :test_framework
-
#
-
2
def remove_invocation(*names)
-
names.each do |name|
-
remove_command(name)
-
remove_class_option(name)
-
invocations.delete(name)
-
invocation_blocks.delete(name)
-
end
-
end
-
-
# Overwrite class options help to allow invoked generators options to be
-
# shown recursively when invoking a generator.
-
#
-
2
def class_options_help(shell, groups = {}) #:nodoc:
-
get_options_from_invocations(groups, class_options) do |klass|
-
klass.send(:get_options_from_invocations, groups, class_options)
-
end
-
super(shell, groups)
-
end
-
-
# Get invocations array and merge options from invocations. Those
-
# options are added to group_options hash. Options that already exists
-
# in base_options are not added twice.
-
#
-
2
def get_options_from_invocations(group_options, base_options) #:nodoc: # rubocop:disable MethodLength
-
invocations.each do |name, from_option|
-
value = if from_option
-
option = class_options[name]
-
option.type == :boolean ? name : option.default
-
else
-
name
-
end
-
next unless value
-
-
klass, _ = prepare_for_invocation(name, value)
-
next unless klass && klass.respond_to?(:class_options)
-
-
value = value.to_s
-
human_name = value.respond_to?(:classify) ? value.classify : value
-
-
group_options[human_name] ||= []
-
group_options[human_name] += klass.class_options.values.select do |class_option|
-
base_options[class_option.name.to_sym].nil? && class_option.group.nil? &&
-
!group_options.values.flatten.any? { |i| i.name == class_option.name }
-
end
-
-
yield klass if block_given?
-
end
-
end
-
-
# Returns commands ready to be printed.
-
2
def printable_commands(*)
-
item = []
-
item << banner
-
item << (desc ? "# #{desc.gsub(/\s+/m, ' ')}" : "")
-
[item]
-
end
-
2
alias_method :printable_tasks, :printable_commands
-
-
2
def handle_argument_error(command, error, args, arity) #:nodoc:
-
msg = "#{basename} #{command.name} takes #{arity} argument"
-
msg << "s" if arity > 1
-
msg << ", but it should not."
-
fail error, msg
-
end
-
-
2
protected
-
-
# The method responsible for dispatching given the args.
-
2
def dispatch(command, given_args, given_opts, config) #:nodoc:
-
if Thor::HELP_MAPPINGS.include?(given_args.first)
-
help(config[:shell])
-
return
-
end
-
-
args, opts = Thor::Options.split(given_args)
-
opts = given_opts || opts
-
-
instance = new(args, opts, config)
-
yield instance if block_given?
-
-
if command
-
instance.invoke_command(all_commands[command])
-
else
-
instance.invoke_all
-
end
-
end
-
-
# The banner for this class. You can customize it if you are invoking the
-
# thor class by another ways which is not the Thor::Runner.
-
2
def banner
-
"#{basename} #{self_command.formatted_usage(self, false)}"
-
end
-
-
# Represents the whole class as a command.
-
2
def self_command #:nodoc:
-
Thor::DynamicCommand.new(namespace, class_options)
-
end
-
2
alias_method :self_task, :self_command
-
-
2
def baseclass #:nodoc:
-
Thor::Group
-
end
-
-
2
def create_command(meth) #:nodoc:
-
commands[meth.to_s] = Thor::Command.new(meth, nil, nil, nil, nil)
-
true
-
end
-
2
alias_method :create_task, :create_command
-
end
-
-
2
include Thor::Base
-
-
2
protected
-
-
# Shortcut to invoke with padding and block handling. Use internally by
-
# invoke and invoke_from_option class methods.
-
2
def _invoke_for_class_method(klass, command = nil, *args, &block) #:nodoc:
-
with_padding do
-
if block
-
case block.arity
-
when 3
-
block.call(self, klass, command)
-
when 2
-
block.call(self, klass)
-
when 1
-
instance_exec(klass, &block)
-
end
-
else
-
invoke klass, command, *args
-
end
-
end
-
end
-
end
-
2
class Thor
-
2
module Invocation
-
2
def self.included(base) #:nodoc:
-
2
base.extend ClassMethods
-
end
-
-
2
module ClassMethods
-
# This method is responsible for receiving a name and find the proper
-
# class and command for it. The key is an optional parameter which is
-
# available only in class methods invocations (i.e. in Thor::Group).
-
2
def prepare_for_invocation(key, name) #:nodoc:
-
case name
-
when Symbol, String
-
Thor::Util.find_class_and_command_by_namespace(name.to_s, !key)
-
else
-
name
-
end
-
end
-
end
-
-
# Make initializer aware of invocations and the initialization args.
-
2
def initialize(args = [], options = {}, config = {}, &block) #:nodoc:
-
@_invocations = config[:invocations] || Hash.new { |h, k| h[k] = [] }
-
@_initializer = [args, options, config]
-
super
-
end
-
-
# Make the current command chain accessible with in a Thor-(sub)command
-
2
def current_command_chain
-
@_invocations.values.flatten.map(&:to_sym)
-
end
-
-
# Receives a name and invokes it. The name can be a string (either "command" or
-
# "namespace:command"), a Thor::Command, a Class or a Thor instance. If the
-
# command cannot be guessed by name, it can also be supplied as second argument.
-
#
-
# You can also supply the arguments, options and configuration values for
-
# the command to be invoked, if none is given, the same values used to
-
# initialize the invoker are used to initialize the invoked.
-
#
-
# When no name is given, it will invoke the default command of the current class.
-
#
-
# ==== Examples
-
#
-
# class A < Thor
-
# def foo
-
# invoke :bar
-
# invoke "b:hello", ["Erik"]
-
# end
-
#
-
# def bar
-
# invoke "b:hello", ["Erik"]
-
# end
-
# end
-
#
-
# class B < Thor
-
# def hello(name)
-
# puts "hello #{name}"
-
# end
-
# end
-
#
-
# You can notice that the method "foo" above invokes two commands: "bar",
-
# which belongs to the same class and "hello" which belongs to the class B.
-
#
-
# By using an invocation system you ensure that a command is invoked only once.
-
# In the example above, invoking "foo" will invoke "b:hello" just once, even
-
# if it's invoked later by "bar" method.
-
#
-
# When class A invokes class B, all arguments used on A initialization are
-
# supplied to B. This allows lazy parse of options. Let's suppose you have
-
# some rspec commands:
-
#
-
# class Rspec < Thor::Group
-
# class_option :mock_framework, :type => :string, :default => :rr
-
#
-
# def invoke_mock_framework
-
# invoke "rspec:#{options[:mock_framework]}"
-
# end
-
# end
-
#
-
# As you noticed, it invokes the given mock framework, which might have its
-
# own options:
-
#
-
# class Rspec::RR < Thor::Group
-
# class_option :style, :type => :string, :default => :mock
-
# end
-
#
-
# Since it's not rspec concern to parse mock framework options, when RR
-
# is invoked all options are parsed again, so RR can extract only the options
-
# that it's going to use.
-
#
-
# If you want Rspec::RR to be initialized with its own set of options, you
-
# have to do that explicitly:
-
#
-
# invoke "rspec:rr", [], :style => :foo
-
#
-
# Besides giving an instance, you can also give a class to invoke:
-
#
-
# invoke Rspec::RR, [], :style => :foo
-
#
-
2
def invoke(name = nil, *args)
-
if name.nil?
-
warn "[Thor] Calling invoke() without argument is deprecated. Please use invoke_all instead.\n#{caller.join("\n")}"
-
return invoke_all
-
end
-
-
args.unshift(nil) if args.first.is_a?(Array) || args.first.nil?
-
command, args, opts, config = args
-
-
klass, command = _retrieve_class_and_command(name, command)
-
fail "Missing Thor class for invoke #{name}" unless klass
-
fail "Expected Thor class, got #{klass}" unless klass <= Thor::Base
-
-
args, opts, config = _parse_initialization_options(args, opts, config)
-
klass.send(:dispatch, command, args, opts, config) do |instance|
-
instance.parent_options = options
-
end
-
end
-
-
# Invoke the given command if the given args.
-
2
def invoke_command(command, *args) #:nodoc:
-
current = @_invocations[self.class]
-
-
unless current.include?(command.name)
-
current << command.name
-
command.run(self, *args)
-
end
-
end
-
2
alias_method :invoke_task, :invoke_command
-
-
# Invoke all commands for the current instance.
-
2
def invoke_all #:nodoc:
-
self.class.all_commands.map { |_, command| invoke_command(command) }
-
end
-
-
# Invokes using shell padding.
-
2
def invoke_with_padding(*args)
-
with_padding { invoke(*args) }
-
end
-
-
2
protected
-
-
# Configuration values that are shared between invocations.
-
2
def _shared_configuration #:nodoc:
-
{:invocations => @_invocations}
-
end
-
-
# This method simply retrieves the class and command to be invoked.
-
# If the name is nil or the given name is a command in the current class,
-
# use the given name and return self as class. Otherwise, call
-
# prepare_for_invocation in the current class.
-
2
def _retrieve_class_and_command(name, sent_command = nil) #:nodoc:
-
case
-
when name.nil?
-
[self.class, nil]
-
when self.class.all_commands[name.to_s]
-
[self.class, name.to_s]
-
else
-
klass, command = self.class.prepare_for_invocation(nil, name)
-
[klass, command || sent_command]
-
end
-
end
-
2
alias_method :_retrieve_class_and_task, :_retrieve_class_and_command
-
-
# Initialize klass using values stored in the @_initializer.
-
2
def _parse_initialization_options(args, opts, config) #:nodoc:
-
stored_args, stored_opts, stored_config = @_initializer
-
-
args ||= stored_args.dup
-
opts ||= stored_opts.dup
-
-
config ||= {}
-
config = stored_config.merge(_shared_configuration).merge!(config)
-
-
[args, opts, config]
-
end
-
end
-
end
-
2
require "thor/line_editor/basic"
-
2
require "thor/line_editor/readline"
-
-
2
class Thor
-
2
module LineEditor
-
2
def self.readline(prompt, options = {})
-
best_available.new(prompt, options).readline
-
end
-
-
2
def self.best_available
-
[
-
Thor::LineEditor::Readline,
-
Thor::LineEditor::Basic
-
].detect(&:available?)
-
end
-
end
-
end
-
2
class Thor
-
2
module LineEditor
-
2
class Basic
-
2
attr_reader :prompt, :options
-
-
2
def self.available?
-
true
-
end
-
-
2
def initialize(prompt, options)
-
@prompt = prompt
-
@options = options
-
end
-
-
2
def readline
-
$stdout.print(prompt)
-
get_input
-
end
-
-
2
private
-
-
2
def get_input
-
if echo?
-
$stdin.gets
-
else
-
$stdin.noecho(&:gets)
-
end
-
end
-
-
2
def echo?
-
options.fetch(:echo, true)
-
end
-
end
-
end
-
end
-
2
begin
-
2
require "readline"
-
rescue LoadError
-
end
-
-
2
class Thor
-
2
module LineEditor
-
2
class Readline < Basic
-
2
def self.available?
-
Object.const_defined?(:Readline)
-
end
-
-
2
def readline
-
if echo?
-
::Readline.completion_append_character = nil
-
# Ruby 1.8.7 does not allow Readline.completion_proc= to receive nil.
-
if complete = completion_proc
-
::Readline.completion_proc = complete
-
end
-
::Readline.readline(prompt, add_to_history?)
-
else
-
super
-
end
-
end
-
-
2
private
-
-
2
def add_to_history?
-
options.fetch(:add_to_history, true)
-
end
-
-
2
def completion_proc
-
if use_path_completion?
-
proc { |text| PathCompletion.new(text).matches }
-
elsif completion_options.any?
-
proc do |text|
-
completion_options.select { |option| option.start_with?(text) }
-
end
-
end
-
end
-
-
2
def completion_options
-
options.fetch(:limited_to, [])
-
end
-
-
2
def use_path_completion?
-
options.fetch(:path, false)
-
end
-
-
2
class PathCompletion
-
2
attr_reader :text
-
2
private :text
-
-
2
def initialize(text)
-
@text = text
-
end
-
-
2
def matches
-
relative_matches
-
end
-
-
2
private
-
-
2
def relative_matches
-
absolute_matches.map { |path| path.sub(base_path, "") }
-
end
-
-
2
def absolute_matches
-
Dir[glob_pattern].map do |path|
-
if File.directory?(path)
-
"#{path}/"
-
else
-
path
-
end
-
end
-
end
-
-
2
def glob_pattern
-
"#{base_path}#{text}*"
-
end
-
-
2
def base_path
-
"#{Dir.pwd}/"
-
end
-
end
-
end
-
end
-
end
-
2
require "thor/parser/argument"
-
2
require "thor/parser/arguments"
-
2
require "thor/parser/option"
-
2
require "thor/parser/options"
-
2
class Thor
-
2
class Argument #:nodoc:
-
2
VALID_TYPES = [:numeric, :hash, :array, :string]
-
-
2
attr_reader :name, :description, :enum, :required, :type, :default, :banner
-
2
alias_method :human_name, :name
-
-
2
def initialize(name, options = {})
-
class_name = self.class.name.split("::").last
-
-
type = options[:type]
-
-
fail ArgumentError, "#{class_name} name can't be nil." if name.nil?
-
fail ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type)
-
-
@name = name.to_s
-
@description = options[:desc]
-
@required = options.key?(:required) ? options[:required] : true
-
@type = (type || :string).to_sym
-
@default = options[:default]
-
@banner = options[:banner] || default_banner
-
@enum = options[:enum]
-
-
validate! # Trigger specific validations
-
end
-
-
2
def usage
-
required? ? banner : "[#{banner}]"
-
end
-
-
2
def required?
-
required
-
end
-
-
2
def show_default?
-
case default
-
when Array, String, Hash
-
!default.empty?
-
else
-
default
-
end
-
end
-
-
2
protected
-
-
2
def validate!
-
if required? && !default.nil?
-
fail ArgumentError, "An argument cannot be required and have default value."
-
elsif @enum && !@enum.is_a?(Array)
-
fail ArgumentError, "An argument cannot have an enum other than an array."
-
end
-
end
-
-
2
def valid_type?(type)
-
self.class::VALID_TYPES.include?(type.to_sym)
-
end
-
-
2
def default_banner
-
case type
-
when :boolean
-
nil
-
when :string, :default
-
human_name.upcase
-
when :numeric
-
"N"
-
when :hash
-
"key:value"
-
when :array
-
"one two three"
-
end
-
end
-
end
-
end
-
2
class Thor
-
2
class Arguments #:nodoc: # rubocop:disable ClassLength
-
2
NUMERIC = /(\d*\.\d+|\d+)/
-
-
# Receives an array of args and returns two arrays, one with arguments
-
# and one with switches.
-
#
-
2
def self.split(args)
-
arguments = []
-
-
args.each do |item|
-
break if item =~ /^-/
-
arguments << item
-
end
-
-
[arguments, args[Range.new(arguments.size, -1)]]
-
end
-
-
2
def self.parse(*args)
-
to_parse = args.pop
-
new(*args).parse(to_parse)
-
end
-
-
# Takes an array of Thor::Argument objects.
-
#
-
2
def initialize(arguments = [])
-
@assigns, @non_assigned_required = {}, []
-
@switches = arguments
-
-
arguments.each do |argument|
-
if !argument.default.nil?
-
@assigns[argument.human_name] = argument.default
-
elsif argument.required?
-
@non_assigned_required << argument
-
end
-
end
-
end
-
-
2
def parse(args)
-
@pile = args.dup
-
-
@switches.each do |argument|
-
break unless peek
-
@non_assigned_required.delete(argument)
-
@assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name)
-
end
-
-
check_requirement!
-
@assigns
-
end
-
-
2
def remaining # rubocop:disable TrivialAccessors
-
@pile
-
end
-
-
2
private
-
-
2
def no_or_skip?(arg)
-
arg =~ /^--(no|skip)-([-\w]+)$/
-
$2
-
end
-
-
2
def last?
-
@pile.empty?
-
end
-
-
2
def peek
-
@pile.first
-
end
-
-
2
def shift
-
@pile.shift
-
end
-
-
2
def unshift(arg)
-
if arg.kind_of?(Array)
-
@pile = arg + @pile
-
else
-
@pile.unshift(arg)
-
end
-
end
-
-
2
def current_is_value?
-
peek && peek.to_s !~ /^-/
-
end
-
-
# Runs through the argument array getting strings that contains ":" and
-
# mark it as a hash:
-
#
-
# [ "name:string", "age:integer" ]
-
#
-
# Becomes:
-
#
-
# { "name" => "string", "age" => "integer" }
-
#
-
2
def parse_hash(name)
-
return shift if peek.is_a?(Hash)
-
hash = {}
-
-
while current_is_value? && peek.include?(":")
-
key, value = shift.split(":", 2)
-
hash[key] = value
-
end
-
hash
-
end
-
-
# Runs through the argument array getting all strings until no string is
-
# found or a switch is found.
-
#
-
# ["a", "b", "c"]
-
#
-
# And returns it as an array:
-
#
-
# ["a", "b", "c"]
-
#
-
2
def parse_array(name)
-
return shift if peek.is_a?(Array)
-
array = []
-
array << shift while current_is_value?
-
array
-
end
-
-
# Check if the peek is numeric format and return a Float or Integer.
-
# Check if the peek is included in enum if enum is provided.
-
# Otherwise raises an error.
-
#
-
2
def parse_numeric(name)
-
return shift if peek.is_a?(Numeric)
-
-
unless peek =~ NUMERIC && $& == peek
-
fail MalformattedArgumentError, "Expected numeric value for '#{name}'; got #{peek.inspect}"
-
end
-
-
value = $&.index(".") ? shift.to_f : shift.to_i
-
if @switches.is_a?(Hash) && switch = @switches[name]
-
if switch.enum && !switch.enum.include?(value)
-
fail MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
-
end
-
end
-
value
-
end
-
-
# Parse string:
-
# for --string-arg, just return the current value in the pile
-
# for --no-string-arg, nil
-
# Check if the peek is included in enum if enum is provided. Otherwise raises an error.
-
#
-
2
def parse_string(name)
-
if no_or_skip?(name)
-
nil
-
else
-
value = shift
-
if @switches.is_a?(Hash) && switch = @switches[name] # rubocop:disable AssignmentInCondition
-
if switch.enum && !switch.enum.include?(value)
-
fail MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}"
-
end
-
end
-
value
-
end
-
end
-
-
# Raises an error if @non_assigned_required array is not empty.
-
#
-
2
def check_requirement!
-
unless @non_assigned_required.empty?
-
names = @non_assigned_required.map do |o|
-
o.respond_to?(:switch_name) ? o.switch_name : o.human_name
-
end.join("', '")
-
-
class_name = self.class.name.split("::").last.downcase
-
fail RequiredArgumentMissingError, "No value provided for required #{class_name} '#{names}'"
-
end
-
end
-
end
-
end
-
2
class Thor
-
2
class Option < Argument #:nodoc:
-
2
attr_reader :aliases, :group, :lazy_default, :hide
-
-
2
VALID_TYPES = [:boolean, :numeric, :hash, :array, :string]
-
-
2
def initialize(name, options = {})
-
options[:required] = false unless options.key?(:required)
-
super
-
@lazy_default = options[:lazy_default]
-
@group = options[:group].to_s.capitalize if options[:group]
-
@aliases = Array(options[:aliases])
-
@hide = options[:hide]
-
end
-
-
# This parse quick options given as method_options. It makes several
-
# assumptions, but you can be more specific using the option method.
-
#
-
# parse :foo => "bar"
-
# #=> Option foo with default value bar
-
#
-
# parse [:foo, :baz] => "bar"
-
# #=> Option foo with default value bar and alias :baz
-
#
-
# parse :foo => :required
-
# #=> Required option foo without default value
-
#
-
# parse :foo => 2
-
# #=> Option foo with default value 2 and type numeric
-
#
-
# parse :foo => :numeric
-
# #=> Option foo without default value and type numeric
-
#
-
# parse :foo => true
-
# #=> Option foo with default value true and type boolean
-
#
-
# The valid types are :boolean, :numeric, :hash, :array and :string. If none
-
# is given a default type is assumed. This default type accepts arguments as
-
# string (--foo=value) or booleans (just --foo).
-
#
-
# By default all options are optional, unless :required is given.
-
#
-
2
def self.parse(key, value) # rubocop:disable MethodLength
-
if key.is_a?(Array)
-
name, *aliases = key
-
else
-
name, aliases = key, []
-
end
-
-
name = name.to_s
-
default = value
-
-
type = case value
-
when Symbol
-
default = nil
-
if VALID_TYPES.include?(value)
-
value
-
elsif required = (value == :required) # rubocop:disable AssignmentInCondition
-
:string
-
end
-
when TrueClass, FalseClass
-
:boolean
-
when Numeric
-
:numeric
-
when Hash, Array, String
-
value.class.name.downcase.to_sym
-
end
-
new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases)
-
end
-
-
2
def switch_name
-
@switch_name ||= dasherized? ? name : dasherize(name)
-
end
-
-
2
def human_name
-
@human_name ||= dasherized? ? undasherize(name) : name
-
end
-
-
2
def usage(padding = 0)
-
sample = if banner && !banner.to_s.empty?
-
"#{switch_name}=#{banner}"
-
else
-
switch_name
-
end
-
-
sample = "[#{sample}]" unless required?
-
-
if boolean?
-
sample << ", [#{dasherize("no-" + human_name)}]" unless name == "force"
-
end
-
-
if aliases.empty?
-
(" " * padding) << sample
-
else
-
"#{aliases.join(', ')}, #{sample}"
-
end
-
end
-
-
2
VALID_TYPES.each do |type|
-
10
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{type}?
-
self.type == #{type.inspect}
-
end
-
RUBY
-
end
-
-
2
protected
-
-
2
def validate!
-
fail ArgumentError, "An option cannot be boolean and required." if boolean? && required?
-
end
-
-
2
def dasherized?
-
name.index("-") == 0
-
end
-
-
2
def undasherize(str)
-
str.sub(/^-{1,2}/, "")
-
end
-
-
2
def dasherize(str)
-
(str.length > 1 ? "--" : "-") + str.gsub("_", "-")
-
end
-
end
-
end
-
2
class Thor
-
2
class Options < Arguments #:nodoc: # rubocop:disable ClassLength
-
2
LONG_RE = /^(--\w+(?:-\w+)*)$/
-
2
SHORT_RE = /^(-[a-z])$/i
-
2
EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i
-
2
SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
-
2
SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
-
2
OPTS_END = "--".freeze
-
-
# Receives a hash and makes it switches.
-
2
def self.to_switches(options)
-
options.map do |key, value|
-
case value
-
when true
-
"--#{key}"
-
when Array
-
"--#{key} #{value.map { |v| v.inspect }.join(' ')}"
-
when Hash
-
"--#{key} #{value.map { |k, v| "#{k}:#{v}" }.join(' ')}"
-
when nil, false
-
""
-
else
-
"--#{key} #{value.inspect}"
-
end
-
end.join(" ")
-
end
-
-
# Takes a hash of Thor::Option and a hash with defaults.
-
#
-
# If +stop_on_unknown+ is true, #parse will stop as soon as it encounters
-
# an unknown option or a regular argument.
-
2
def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false)
-
@stop_on_unknown = stop_on_unknown
-
options = hash_options.values
-
super(options)
-
-
# Add defaults
-
defaults.each do |key, value|
-
@assigns[key.to_s] = value
-
@non_assigned_required.delete(hash_options[key])
-
end
-
-
@shorts, @switches, @extra = {}, {}, []
-
-
options.each do |option|
-
@switches[option.switch_name] = option
-
-
option.aliases.each do |short|
-
name = short.to_s.sub(/^(?!\-)/, "-")
-
@shorts[name] ||= option.switch_name
-
end
-
end
-
end
-
-
2
def remaining # rubocop:disable TrivialAccessors
-
@extra
-
end
-
-
2
def peek
-
return super unless @parsing_options
-
-
result = super
-
if result == OPTS_END
-
shift
-
@parsing_options = false
-
super
-
else
-
result
-
end
-
end
-
-
2
def parse(args) # rubocop:disable MethodLength
-
@pile = args.dup
-
@parsing_options = true
-
-
while peek
-
if parsing_options?
-
match, is_switch = current_is_switch?
-
shifted = shift
-
-
if is_switch
-
case shifted
-
when SHORT_SQ_RE
-
unshift($1.split("").map { |f| "-#{f}" })
-
next
-
when EQ_RE, SHORT_NUM
-
unshift($2)
-
switch = $1
-
when LONG_RE, SHORT_RE
-
switch = $1
-
end
-
-
switch = normalize_switch(switch)
-
option = switch_option(switch)
-
@assigns[option.human_name] = parse_peek(switch, option)
-
elsif @stop_on_unknown
-
@parsing_options = false
-
@extra << shifted
-
@extra << shift while peek
-
break
-
elsif match
-
@extra << shifted
-
@extra << shift while peek && peek !~ /^-/
-
else
-
@extra << shifted
-
end
-
else
-
@extra << shift
-
end
-
end
-
-
check_requirement!
-
-
assigns = Thor::CoreExt::HashWithIndifferentAccess.new(@assigns)
-
assigns.freeze
-
assigns
-
end
-
-
2
def check_unknown!
-
# an unknown option starts with - or -- and has no more --'s afterward.
-
unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ }
-
fail UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty?
-
end
-
-
2
protected
-
-
# Check if the current value in peek is a registered switch.
-
#
-
# Two booleans are returned. The first is true if the current value
-
# starts with a hyphen; the second is true if it is a registered switch.
-
2
def current_is_switch?
-
case peek
-
when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
-
[true, switch?($1)]
-
when SHORT_SQ_RE
-
[true, $1.split("").any? { |f| switch?("-#{f}") }]
-
else
-
[false, false]
-
end
-
end
-
-
2
def current_is_switch_formatted?
-
case peek
-
when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE
-
true
-
else
-
false
-
end
-
end
-
-
2
def current_is_value?
-
peek && (!parsing_options? || super)
-
end
-
-
2
def switch?(arg)
-
switch_option(normalize_switch(arg))
-
end
-
-
2
def switch_option(arg)
-
if match = no_or_skip?(arg) # rubocop:disable AssignmentInCondition
-
@switches[arg] || @switches["--#{match}"]
-
else
-
@switches[arg]
-
end
-
end
-
-
# Check if the given argument is actually a shortcut.
-
#
-
2
def normalize_switch(arg)
-
(@shorts[arg] || arg).tr("_", "-")
-
end
-
-
2
def parsing_options?
-
peek
-
@parsing_options
-
end
-
-
# Parse boolean values which can be given as --foo=true, --foo or --no-foo.
-
#
-
2
def parse_boolean(switch)
-
if current_is_value?
-
if ["true", "TRUE", "t", "T", true].include?(peek)
-
shift
-
true
-
elsif ["false", "FALSE", "f", "F", false].include?(peek)
-
shift
-
false
-
else
-
true
-
end
-
else
-
@switches.key?(switch) || !no_or_skip?(switch)
-
end
-
end
-
-
# Parse the value at the peek analyzing if it requires an input or not.
-
#
-
2
def parse_peek(switch, option)
-
if parsing_options? && (current_is_switch_formatted? || last?)
-
if option.boolean?
-
# No problem for boolean types
-
elsif no_or_skip?(switch)
-
return nil # User set value to nil
-
elsif option.string? && !option.required?
-
# Return the default if there is one, else the human name
-
return option.lazy_default || option.default || option.human_name
-
elsif option.lazy_default
-
return option.lazy_default
-
else
-
fail MalformattedArgumentError, "No value provided for option '#{switch}'"
-
end
-
end
-
-
@non_assigned_required.delete(option)
-
send(:"parse_#{option.type}", switch)
-
end
-
end
-
end
-
2
require "rbconfig"
-
-
2
class Thor
-
2
module Base
-
2
class << self
-
2
attr_writer :shell
-
-
# Returns the shell used in all Thor classes. If you are in a Unix platform
-
# it will use a colored log, otherwise it will use a basic one without color.
-
#
-
2
def shell
-
@shell ||= if ENV["THOR_SHELL"] && ENV["THOR_SHELL"].size > 0
-
Thor::Shell.const_get(ENV["THOR_SHELL"])
-
elsif RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ && !ENV["ANSICON"]
-
Thor::Shell::Basic
-
else
-
Thor::Shell::Color
-
end
-
end
-
end
-
end
-
-
2
module Shell
-
2
SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_status, :print_in_columns, :print_table, :print_wrapped, :file_collision, :terminal_width]
-
2
attr_writer :shell
-
-
2
autoload :Basic, "thor/shell/basic"
-
2
autoload :Color, "thor/shell/color"
-
2
autoload :HTML, "thor/shell/html"
-
-
# Add shell to initialize config values.
-
#
-
# ==== Configuration
-
# shell<Object>:: An instance of the shell to be used.
-
#
-
# ==== Examples
-
#
-
# class MyScript < Thor
-
# argument :first, :type => :numeric
-
# end
-
#
-
# MyScript.new [1.0], { :foo => :bar }, :shell => Thor::Shell::Basic.new
-
#
-
2
def initialize(args = [], options = {}, config = {})
-
super
-
self.shell = config[:shell]
-
shell.base ||= self if shell.respond_to?(:base)
-
end
-
-
# Holds the shell for the given Thor instance. If no shell is given,
-
# it gets a default shell from Thor::Base.shell.
-
2
def shell
-
@shell ||= Thor::Base.shell.new
-
end
-
-
# Common methods that are delegated to the shell.
-
2
SHELL_DELEGATED_METHODS.each do |method|
-
24
module_eval <<-METHOD, __FILE__, __LINE__
-
def #{method}(*args,&block)
-
shell.#{method}(*args,&block)
-
end
-
METHOD
-
end
-
-
# Yields the given block with padding.
-
2
def with_padding
-
shell.padding += 1
-
yield
-
ensure
-
shell.padding -= 1
-
end
-
-
2
protected
-
-
# Allow shell to be shared between invocations.
-
#
-
2
def _shared_configuration #:nodoc:
-
super.merge!(:shell => shell)
-
end
-
end
-
end
-
2
require "tempfile"
-
2
require "io/console" if RUBY_VERSION > "1.9.2"
-
-
2
class Thor
-
2
module Shell
-
2
class Basic # rubocop:disable ClassLength
-
2
attr_accessor :base
-
2
attr_reader :padding
-
-
# Initialize base, mute and padding to nil.
-
#
-
2
def initialize #:nodoc:
-
@base, @mute, @padding, @always_force = nil, false, 0, false
-
end
-
-
# Mute everything that's inside given block
-
#
-
2
def mute
-
@mute = true
-
yield
-
ensure
-
@mute = false
-
end
-
-
# Check if base is muted
-
#
-
2
def mute? # rubocop:disable TrivialAccessors
-
@mute
-
end
-
-
# Sets the output padding, not allowing less than zero values.
-
#
-
2
def padding=(value)
-
@padding = [0, value].max
-
end
-
-
# Asks something to the user and receives a response.
-
#
-
# If asked to limit the correct responses, you can pass in an
-
# array of acceptable answers. If one of those is not supplied,
-
# they will be shown a message stating that one of those answers
-
# must be given and re-asked the question.
-
#
-
# If asking for sensitive information, the :echo option can be set
-
# to false to mask user input from $stdin.
-
#
-
# If the required input is a path, then set the path option to
-
# true. This will enable tab completion for file paths relative
-
# to the current working directory on systems that support
-
# Readline.
-
#
-
# ==== Example
-
# ask("What is your name?")
-
#
-
# ask("What is your favorite Neopolitan flavor?", :limited_to => ["strawberry", "chocolate", "vanilla"])
-
#
-
# ask("What is your password?", :echo => false)
-
#
-
# ask("Where should the file be saved?", :path => true)
-
#
-
2
def ask(statement, *args)
-
options = args.last.is_a?(Hash) ? args.pop : {}
-
color = args.first
-
-
if options[:limited_to]
-
ask_filtered(statement, color, options)
-
else
-
ask_simply(statement, color, options)
-
end
-
end
-
-
# Say (print) something to the user. If the sentence ends with a whitespace
-
# or tab character, a new line is not appended (print + flush). Otherwise
-
# are passed straight to puts (behavior got from Highline).
-
#
-
# ==== Example
-
# say("I know you knew that.")
-
#
-
2
def say(message = "", color = nil, force_new_line = (message.to_s !~ /( |\t)\Z/))
-
buffer = prepare_message(message, *color)
-
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
-
-
stdout.print(buffer)
-
stdout.flush
-
end
-
-
# Say a status with the given color and appends the message. Since this
-
# method is used frequently by actions, it allows nil or false to be given
-
# in log_status, avoiding the message from being shown. If a Symbol is
-
# given in log_status, it's used as the color.
-
#
-
2
def say_status(status, message, log_status = true)
-
return if quiet? || log_status == false
-
spaces = " " * (padding + 1)
-
color = log_status.is_a?(Symbol) ? log_status : :green
-
-
status = status.to_s.rjust(12)
-
status = set_color status, color, true if color
-
-
buffer = "#{status}#{spaces}#{message}"
-
buffer << "\n" unless buffer.end_with?("\n")
-
-
stdout.print(buffer)
-
stdout.flush
-
end
-
-
# Make a question the to user and returns true if the user replies "y" or
-
# "yes".
-
#
-
2
def yes?(statement, color = nil)
-
!!(ask(statement, color, :add_to_history => false) =~ is?(:yes))
-
end
-
-
# Make a question the to user and returns true if the user replies "n" or
-
# "no".
-
#
-
2
def no?(statement, color = nil)
-
!!(ask(statement, color, :add_to_history => false) =~ is?(:no))
-
end
-
-
# Prints values in columns
-
#
-
# ==== Parameters
-
# Array[String, String, ...]
-
#
-
2
def print_in_columns(array)
-
return if array.empty?
-
colwidth = (array.map { |el| el.to_s.size }.max || 0) + 2
-
array.each_with_index do |value, index|
-
# Don't output trailing spaces when printing the last column
-
if ((((index + 1) % (terminal_width / colwidth))).zero? && !index.zero?) || index + 1 == array.length
-
stdout.puts value
-
else
-
stdout.printf("%-#{colwidth}s", value)
-
end
-
end
-
end
-
-
# Prints a table.
-
#
-
# ==== Parameters
-
# Array[Array[String, String, ...]]
-
#
-
# ==== Options
-
# indent<Integer>:: Indent the first column by indent value.
-
# colwidth<Integer>:: Force the first column to colwidth spaces wide.
-
#
-
2
def print_table(array, options = {}) # rubocop:disable MethodLength
-
return if array.empty?
-
-
formats, indent, colwidth = [], options[:indent].to_i, options[:colwidth]
-
options[:truncate] = terminal_width if options[:truncate] == true
-
-
formats << "%-#{colwidth + 2}s" if colwidth
-
start = colwidth ? 1 : 0
-
-
colcount = array.max { |a, b| a.size <=> b.size }.size
-
-
maximas = []
-
-
start.upto(colcount - 1) do |index|
-
maxima = array.map { |row| row[index] ? row[index].to_s.size : 0 }.max
-
maximas << maxima
-
if index == colcount - 1
-
# Don't output 2 trailing spaces when printing the last column
-
formats << "%-s"
-
else
-
formats << "%-#{maxima + 2}s"
-
end
-
end
-
-
formats[0] = formats[0].insert(0, " " * indent)
-
formats << "%s"
-
-
array.each do |row|
-
sentence = ""
-
-
row.each_with_index do |column, index|
-
maxima = maximas[index]
-
-
if column.is_a?(Numeric)
-
if index == row.size - 1
-
# Don't output 2 trailing spaces when printing the last column
-
f = "%#{maxima}s"
-
else
-
f = "%#{maxima}s "
-
end
-
else
-
f = formats[index]
-
end
-
sentence << f % column.to_s
-
end
-
-
sentence = truncate(sentence, options[:truncate]) if options[:truncate]
-
stdout.puts sentence
-
end
-
end
-
-
# Prints a long string, word-wrapping the text to the current width of the
-
# terminal display. Ideal for printing heredocs.
-
#
-
# ==== Parameters
-
# String
-
#
-
# ==== Options
-
# indent<Integer>:: Indent each line of the printed paragraph by indent value.
-
#
-
2
def print_wrapped(message, options = {})
-
indent = options[:indent] || 0
-
width = terminal_width - indent
-
paras = message.split("\n\n")
-
-
paras.map! do |unwrapped|
-
unwrapped.strip.gsub(/\n/, " ").squeeze(" ").gsub(/.{1,#{width}}(?:\s|\Z)/) { ($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n") }
-
end
-
-
paras.each do |para|
-
para.split("\n").each do |line|
-
stdout.puts line.insert(0, " " * indent)
-
end
-
stdout.puts unless para == paras.last
-
end
-
end
-
-
# Deals with file collision and returns true if the file should be
-
# overwritten and false otherwise. If a block is given, it uses the block
-
# response as the content for the diff.
-
#
-
# ==== Parameters
-
# destination<String>:: the destination file to solve conflicts
-
# block<Proc>:: an optional block that returns the value to be used in diff
-
#
-
2
def file_collision(destination) # rubocop:disable MethodLength
-
return true if @always_force
-
options = block_given? ? "[Ynaqdh]" : "[Ynaqh]"
-
-
loop do
-
answer = ask(
-
%[Overwrite #{destination}? (enter "h" for help) #{options}],
-
:add_to_history => false
-
)
-
-
case answer
-
when is?(:yes), is?(:force), ""
-
return true
-
when is?(:no), is?(:skip)
-
return false
-
when is?(:always)
-
return @always_force = true
-
when is?(:quit)
-
say "Aborting..."
-
fail SystemExit
-
when is?(:diff)
-
show_diff(destination, yield) if block_given?
-
say "Retrying..."
-
else
-
say file_collision_help
-
end
-
end
-
end
-
-
# This code was copied from Rake, available under MIT-LICENSE
-
# Copyright (c) 2003, 2004 Jim Weirich
-
2
def terminal_width
-
if ENV["THOR_COLUMNS"]
-
result = ENV["THOR_COLUMNS"].to_i
-
else
-
result = unix? ? dynamic_width : 80
-
end
-
result < 10 ? 80 : result
-
rescue
-
80
-
end
-
-
# Called if something goes wrong during the execution. This is used by Thor
-
# internally and should not be used inside your scripts. If something went
-
# wrong, you can always raise an exception. If you raise a Thor::Error, it
-
# will be rescued and wrapped in the method below.
-
#
-
2
def error(statement)
-
stderr.puts statement
-
end
-
-
# Apply color to the given string with optional bold. Disabled in the
-
# Thor::Shell::Basic class.
-
#
-
2
def set_color(string, *args) #:nodoc:
-
string
-
end
-
-
2
protected
-
-
2
def prepare_message(message, *color)
-
spaces = " " * padding
-
spaces + set_color(message.to_s, *color)
-
end
-
-
2
def can_display_colors?
-
false
-
end
-
-
2
def lookup_color(color)
-
return color unless color.is_a?(Symbol)
-
self.class.const_get(color.to_s.upcase)
-
end
-
-
2
def stdout
-
$stdout
-
end
-
-
2
def stderr
-
$stderr
-
end
-
-
2
def is?(value) #:nodoc:
-
value = value.to_s
-
-
if value.size == 1
-
/\A#{value}\z/i
-
else
-
/\A(#{value}|#{value[0, 1]})\z/i
-
end
-
end
-
-
2
def file_collision_help #:nodoc:
-
<<-HELP
-
Y - yes, overwrite
-
n - no, do not overwrite
-
a - all, overwrite this and all others
-
q - quit, abort
-
d - diff, show the differences between the old and the new
-
h - help, show this help
-
HELP
-
end
-
-
2
def show_diff(destination, content) #:nodoc:
-
diff_cmd = ENV["THOR_DIFF"] || ENV["RAILS_DIFF"] || "diff -u"
-
-
Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp|
-
temp.write content
-
temp.rewind
-
system %(#{diff_cmd} "#{destination}" "#{temp.path}")
-
end
-
end
-
-
2
def quiet? #:nodoc:
-
mute? || (base && base.options[:quiet])
-
end
-
-
# Calculate the dynamic width of the terminal
-
2
def dynamic_width
-
@dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput)
-
end
-
-
2
def dynamic_width_stty
-
%x(stty size 2>/dev/null).split[1].to_i
-
end
-
-
2
def dynamic_width_tput
-
%x(tput cols 2>/dev/null).to_i
-
end
-
-
2
def unix?
-
RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i
-
end
-
-
2
def truncate(string, width)
-
as_unicode do
-
chars = string.chars.to_a
-
if chars.length <= width
-
chars.join
-
else
-
( chars[0, width - 3].join) + "..."
-
end
-
end
-
end
-
-
2
if "".respond_to?(:encode)
-
2
def as_unicode
-
yield
-
end
-
else
-
def as_unicode
-
old, $KCODE = $KCODE, "U"
-
yield
-
ensure
-
$KCODE = old
-
end
-
end
-
-
2
def ask_simply(statement, color, options)
-
default = options[:default]
-
message = [statement, ("(#{default})" if default), nil].uniq.join(" ")
-
message = prepare_message(message, color)
-
result = Thor::LineEditor.readline(message, options)
-
-
return unless result
-
-
result.strip!
-
-
if default && result == ""
-
default
-
else
-
result
-
end
-
end
-
-
2
def ask_filtered(statement, color, options)
-
answer_set = options[:limited_to]
-
correct_answer = nil
-
until correct_answer
-
answers = answer_set.join(", ")
-
answer = ask_simply("#{statement} [#{answers}]", color, options)
-
correct_answer = answer_set.include?(answer) ? answer : nil
-
say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer
-
end
-
correct_answer
-
end
-
end
-
end
-
end
-
2
require "rbconfig"
-
-
2
class Thor
-
2
module Sandbox #:nodoc:
-
end
-
-
# This module holds several utilities:
-
#
-
# 1) Methods to convert thor namespaces to constants and vice-versa.
-
#
-
# Thor::Util.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz"
-
#
-
# 2) Loading thor files and sandboxing:
-
#
-
# Thor::Util.load_thorfile("~/.thor/foo")
-
#
-
2
module Util
-
2
class << self
-
# Receives a namespace and search for it in the Thor::Base subclasses.
-
#
-
# ==== Parameters
-
# namespace<String>:: The namespace to search for.
-
#
-
2
def find_by_namespace(namespace)
-
namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/
-
Thor::Base.subclasses.detect { |klass| klass.namespace == namespace }
-
end
-
-
# Receives a constant and converts it to a Thor namespace. Since Thor
-
# commands can be added to a sandbox, this method is also responsable for
-
# removing the sandbox namespace.
-
#
-
# This method should not be used in general because it's used to deal with
-
# older versions of Thor. On current versions, if you need to get the
-
# namespace from a class, just call namespace on it.
-
#
-
# ==== Parameters
-
# constant<Object>:: The constant to be converted to the thor path.
-
#
-
# ==== Returns
-
# String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz"
-
#
-
2
def namespace_from_thor_class(constant)
-
constant = constant.to_s.gsub(/^Thor::Sandbox::/, "")
-
constant = snake_case(constant).squeeze(":")
-
constant
-
end
-
-
# Given the contents, evaluate it inside the sandbox and returns the
-
# namespaces defined in the sandbox.
-
#
-
# ==== Parameters
-
# contents<String>
-
#
-
# ==== Returns
-
# Array[Object]
-
#
-
2
def namespaces_in_content(contents, file = __FILE__)
-
old_constants = Thor::Base.subclasses.dup
-
Thor::Base.subclasses.clear
-
-
load_thorfile(file, contents)
-
-
new_constants = Thor::Base.subclasses.dup
-
Thor::Base.subclasses.replace(old_constants)
-
-
new_constants.map! { |c| c.namespace }
-
new_constants.compact!
-
new_constants
-
end
-
-
# Returns the thor classes declared inside the given class.
-
#
-
2
def thor_classes_in(klass)
-
stringfied_constants = klass.constants.map { |c| c.to_s }
-
Thor::Base.subclasses.select do |subclass|
-
next unless subclass.name
-
stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", ""))
-
end
-
end
-
-
# Receives a string and convert it to snake case. SnakeCase returns snake_case.
-
#
-
# ==== Parameters
-
# String
-
#
-
# ==== Returns
-
# String
-
#
-
2
def snake_case(str)
-
return str.downcase if str =~ /^[A-Z_]+$/
-
str.gsub(/\B[A-Z]/, '_\&').squeeze("_") =~ /_*(.*)/
-
$+.downcase
-
end
-
-
# Receives a string and convert it to camel case. camel_case returns CamelCase.
-
#
-
# ==== Parameters
-
# String
-
#
-
# ==== Returns
-
# String
-
#
-
2
def camel_case(str)
-
return str if str !~ /_/ && str =~ /[A-Z]+.*/
-
str.split("_").map { |i| i.capitalize }.join
-
end
-
-
# Receives a namespace and tries to retrieve a Thor or Thor::Group class
-
# from it. It first searches for a class using the all the given namespace,
-
# if it's not found, removes the highest entry and searches for the class
-
# again. If found, returns the highest entry as the class name.
-
#
-
# ==== Examples
-
#
-
# class Foo::Bar < Thor
-
# def baz
-
# end
-
# end
-
#
-
# class Baz::Foo < Thor::Group
-
# end
-
#
-
# Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default command
-
# Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil
-
# Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz"
-
#
-
# ==== Parameters
-
# namespace<String>
-
#
-
2
def find_class_and_command_by_namespace(namespace, fallback = true)
-
if namespace.include?(":") # look for a namespaced command
-
pieces = namespace.split(":")
-
command = pieces.pop
-
klass = Thor::Util.find_by_namespace(pieces.join(":"))
-
end
-
unless klass # look for a Thor::Group with the right name
-
klass, command = Thor::Util.find_by_namespace(namespace), nil
-
end
-
if !klass && fallback # try a command in the default namespace
-
command = namespace
-
klass = Thor::Util.find_by_namespace("")
-
end
-
[klass, command]
-
end
-
2
alias_method :find_class_and_task_by_namespace, :find_class_and_command_by_namespace
-
-
# Receives a path and load the thor file in the path. The file is evaluated
-
# inside the sandbox to avoid namespacing conflicts.
-
#
-
2
def load_thorfile(path, content = nil, debug = false)
-
content ||= File.binread(path)
-
-
begin
-
Thor::Sandbox.class_eval(content, path)
-
rescue StandardError => e
-
$stderr.puts("WARNING: unable to load thorfile #{path.inspect}: #{e.message}")
-
if debug
-
$stderr.puts(*e.backtrace)
-
else
-
$stderr.puts(e.backtrace.first)
-
end
-
end
-
end
-
-
2
def user_home # rubocop:disable MethodLength
-
@@user_home ||= if ENV["HOME"]
-
ENV["HOME"]
-
elsif ENV["USERPROFILE"]
-
ENV["USERPROFILE"]
-
elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
-
File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"])
-
elsif ENV["APPDATA"]
-
ENV["APPDATA"]
-
else
-
begin
-
File.expand_path("~")
-
rescue
-
if File::ALT_SEPARATOR
-
"C:/"
-
else
-
"/"
-
end
-
end
-
end
-
end
-
-
# Returns the root where thor files are located, depending on the OS.
-
#
-
2
def thor_root
-
File.join(user_home, ".thor").gsub(/\\/, "/")
-
end
-
-
# Returns the files in the thor root. On Windows thor_root will be something
-
# like this:
-
#
-
# C:\Documents and Settings\james\.thor
-
#
-
# If we don't #gsub the \ character, Dir.glob will fail.
-
#
-
2
def thor_root_glob
-
files = Dir["#{escape_globs(thor_root)}/*"]
-
-
files.map! do |file|
-
File.directory?(file) ? File.join(file, "main.thor") : file
-
end
-
end
-
-
# Where to look for Thor files.
-
#
-
2
def globs_for(path)
-
path = escape_globs(path)
-
["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
-
end
-
-
# Return the path to the ruby interpreter taking into account multiple
-
# installations and windows extensions.
-
#
-
2
def ruby_command # rubocop:disable MethodLength
-
@ruby_command ||= begin
-
ruby_name = RbConfig::CONFIG["ruby_install_name"]
-
ruby = File.join(RbConfig::CONFIG["bindir"], ruby_name)
-
ruby << RbConfig::CONFIG["EXEEXT"]
-
-
# avoid using different name than ruby (on platforms supporting links)
-
if ruby_name != "ruby" && File.respond_to?(:readlink)
-
begin
-
alternate_ruby = File.join(RbConfig::CONFIG["bindir"], "ruby")
-
alternate_ruby << RbConfig::CONFIG["EXEEXT"]
-
-
# ruby is a symlink
-
if File.symlink? alternate_ruby
-
linked_ruby = File.readlink alternate_ruby
-
-
# symlink points to 'ruby_install_name'
-
ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby
-
end
-
rescue NotImplementedError # rubocop:disable HandleExceptions
-
# just ignore on windows
-
end
-
end
-
-
# escape string in case path to ruby executable contain spaces.
-
ruby.sub!(/.*\s.*/m, '"\&"')
-
ruby
-
end
-
end
-
-
# Returns a string that has had any glob characters escaped.
-
# The glob characters are `* ? { } [ ]`.
-
#
-
# ==== Examples
-
#
-
# Thor::Util.escape_globs('[apps]') # => '\[apps\]'
-
#
-
# ==== Parameters
-
# String
-
#
-
# ==== Returns
-
# String
-
#
-
2
def escape_globs(path)
-
path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&')
-
end
-
end
-
end
-
end
-
2
class ApplicationController < ActionController::Base
-
# Prevent CSRF attacks by raising an exception.
-
# For APIs, you may want to use :null_session instead.
-
2
protect_from_forgery with: :exception
-
2
before_action :configure_permitted_parameters, if: :devise_controller?
-
-
2
protected
-
-
2
def configure_permitted_parameters
-
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :roll])
-
devise_parameter_sanitizer.permit(:sign_in) do |user_params|
-
user_params.permit(:roll, :first_name, :last_name, :email)
-
end
-
end
-
-
2
def authorize_admin
-
10
redirect_to root_url unless current_user.admin?
-
end
-
end
-
1
class CoursesController < ApplicationController
-
-
1
before_filter :authenticate_user!, except: [:index, :show]
-
1
before_action :set_course, only: [:show, :edit, :update, :destroy]
-
1
before_filter :authorize_admin, only: [:new, :edit, :update, :destroy]
-
-
# GET /courses
-
# GET /courses.json
-
1
def index
-
1
@current_semester = Semester.find_by(status: 'Current')
-
1
completed_semesters = Semester.where(status: 'Done')
-
1
upcoming_semesters = Semester.where(status: 'Due')
-
-
1
@current = Course.where(semester: @current_semester.sem_name)
-
1
@completed = Course.where(semester: completed_semesters.pluck(:sem_name))
-
1
@upcoming = Course.where(semester: upcoming_semesters.pluck(:sem_name))
-
end
-
-
# GET /courses/1
-
# GET /courses/1.json
-
1
def show
-
end
-
-
# GET /courses/new
-
1
def new
-
1
@course = Course.new
-
end
-
-
# GET /courses/1/edit
-
1
def edit
-
end
-
-
# POST /courses
-
# POST /courses.json
-
1
def create
-
2
@course = Course.new(course_params)
-
-
2
respond_to do |format|
-
2
if @course.save
-
#redirect_to courses_path
-
2
format.html { redirect_to courses_path, notice: 'Course was successfully created.' }
-
1
format.json { render :show, status: :created, location: @course }
-
else
-
2
format.html { render :new }
-
1
format.json { render json: @course.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# PATCH/PUT /courses/1
-
# PATCH/PUT /courses/1.json
-
1
def update
-
2
respond_to do |format|
-
2
if @course.update(course_params)
-
format.html { redirect_to courses_path, notice: 'Course was successfully updated.' }
-
format.json { render :show, status: :ok, location: @course }
-
else
-
4
format.html { render :edit }
-
2
format.json { render json: @course.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# DELETE /courses/1
-
# DELETE /courses/1.json
-
1
def destroy
-
1
@course.destroy
-
1
respond_to do |format|
-
2
format.html { redirect_to courses_url, notice: 'Course was successfully destroyed.' }
-
1
format.json { head :no_content }
-
end
-
end
-
-
1
private
-
# Use callbacks to share common setup or constraints between actions.
-
1
def set_course
-
5
@course = Course.find(params[:id])
-
end
-
-
# Never trust parameters from the scary internet, only allow the white list through.
-
1
def course_params
-
#params.fetch(:course, {})
-
4
params.require(:course).permit(:course_no, :course_title, :credit, :semester)
-
end
-
end
-
2
class EnrollsController < ApplicationController
-
2
before_filter :authenticate_user!, except: [:index, :show]
-
2
before_action :set_enroll, only: [:show, :edit, :update, :destroy]
-
2
respond_to :html, :js
-
-
# GET /enrolls
-
# GET /enrolls.json
-
2
def index
-
@enrolls = Enroll.all
-
end
-
-
# GET /enrolls/1
-
# GET /enrolls/1.json
-
2
def show
-
@enroll = Enroll.find(params[:id])
-
end
-
-
# GET /enrolls/new
-
2
def new
-
@user = current_user
-
@semester = Semester.find_by_status("Current")
-
@maximum_credit = @semester.maximum_credit
-
@courses = Course.where("semester = ?", @semester.sem_name)
-
@enroll = Enroll.new(user_id: @user.id, semester_id: @semester.id)
-
@courses.each do |course|
-
@enroll.registers.new(course_id: course.id)
-
end
-
end
-
-
# GET /enrolls/1/edit
-
2
def edit
-
@user = current_user
-
@semester = Semester.find_by_status("Current")
-
@maximum_credit = @semester.maximum_credit
-
@courses = Course.where("semester = ?", @semester.sem_name)
-
end
-
-
# POST /enrolls
-
# POST /enrolls.json
-
2
def create
-
@enroll = Enroll.new(enroll_params)
-
-
# params[:enroll][:course_ids].each do |c_id|
-
# @enroll.registers.new(course_id: c_id) if c_id.present?
-
# end
-
-
respond_to do |format|
-
if @enroll.save
-
# params[:course_attributes].each do |course|
-
# @enroll.registers.create(course_id: course['course_id'], enroll_id: @enroll.id)
-
# end
-
format.html { redirect_to @enroll, notice: 'Enroll was successfully created.' }
-
format.js { }
-
format.json { render :show, status: :created, location: @enroll }
-
else
-
@user = current_user
-
@semester = Semester.find_by_status("Current")
-
@courses = Course.where("semester = ?", @semester.sem_name)
-
@courses.each do |course|
-
@enroll.registers.new(course_id: course.id)
-
end
-
format.html { render :edit }
-
format.json { render new_enroll_path @enroll.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# PATCH/PUT /enrolls/1
-
# PATCH/PUT /enrolls/1.json
-
2
def update
-
@user = current_user
-
@semester = Semester.find_by_status("Current")
-
@maximum_credit = @semester.maximum_credit
-
@courses = Course.where("semester = ?", @semester.sem_name)
-
respond_to do |format|
-
if @enroll.update(enroll_params)
-
format.html { redirect_to @enroll, notice: 'Enroll was successfully updated.' }
-
format.json { render :show, status: :ok, location: @enroll }
-
else
-
format.html { render :edit }
-
format.json { render json: @enroll.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# DELETE /enrolls/1
-
# DELETE /enrolls/1.json
-
2
def destroy
-
@enroll.destroy
-
respond_to do |format|
-
format.html { redirect_to enrolls_url, notice: 'Enroll was successfully destroyed.' }
-
format.json { head :no_content }
-
end
-
end
-
-
2
private
-
# Use callbacks to share common setup or constraints between actions.
-
2
def set_enroll
-
@enroll = Enroll.find(params[:id])
-
end
-
-
# Never trust parameters from the scary internet, only allow the white list through.
-
2
def enroll_params
-
#params.require(:enroll).permit(:user_id, :course_id, :semester_id, :user_attributes, :semester_attributes, registers_attributes: [course_attributes:[]])
-
params.require(:enroll).permit(:user_id, :semester_id, :status, course_ids:[])
-
end
-
end
-
1
class RegistersController < ApplicationController
-
1
before_filter :authenticate_user!, except: [:index, :show]
-
1
before_action :set_register, only: [:show, :edit, :update, :destroy]
-
-
# GET /registers
-
# GET /registers.json
-
1
def index
-
1
@registers = Register.all
-
-
1
@current_semester = Semester.find_by_status("Current")
-
1
@completed_semesters = Semester.find_by_status("Done")
-
1
@upcoming_semesters = Semester.find_by_status("Due")
-
1
@current_enroll_id = @current_semester.enrolls.ids
-
1
@completed_enroll_ids = @completed_semesters.enrolls.ids
-
1
@upcoming_enroll_ids = @upcoming_semesters.enrolls.ids
-
end
-
-
# GET /registers/1
-
# GET /registers/1.json
-
1
def show
-
end
-
-
# GET /registers/new
-
1
def new
-
1
@register = Register.new
-
end
-
-
# GET /registers/1/edit
-
1
def edit
-
end
-
-
# POST /registers
-
# POST /registers.json
-
1
def create
-
4
@register = Register.new(register_params)
-
-
4
respond_to do |format|
-
4
if @register.save
-
8
format.html { redirect_to @register, notice: 'Register was successfully created.' }
-
4
format.json { render :show, status: :created, location: @register }
-
else
-
format.html { render :new }
-
format.json { render json: @register.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# PATCH/PUT /registers/1
-
# PATCH/PUT /registers/1.json
-
1
def update
-
2
respond_to do |format|
-
2
if @register.update(register_params)
-
4
format.html { redirect_to @register, notice: 'Register was successfully updated.' }
-
2
format.json { render :show, status: :ok, location: @register }
-
else
-
format.html { render :edit }
-
format.json { render json: @register.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# DELETE /registers/1
-
# DELETE /registers/1.json
-
1
def destroy
-
1
@register.destroy
-
1
respond_to do |format|
-
2
format.html { redirect_to registers_url, notice: 'Register was successfully destroyed.' }
-
1
format.json { head :no_content }
-
end
-
end
-
-
1
private
-
# Use callbacks to share common setup or constraints between actions.
-
1
def set_register
-
5
@register = Register.find(params[:id])
-
end
-
-
# Never trust parameters from the scary internet, only allow the white list through.
-
1
def register_params
-
6
params.require(:register).permit(:enroll, :course_id)
-
end
-
end
-
1
class SemestersController < ApplicationController
-
1
before_filter :authenticate_user!, except: [:index, :show]
-
1
before_action :set_semester, only: [:show, :edit, :update, :destroy]
-
1
before_filter :authorize_admin, only: [:new, :edit, :update, :destroy]
-
-
-
# GET /semesters
-
# GET /semesters.json
-
1
def index
-
1
@semesters = Semester.all
-
end
-
-
# GET /semesters/1
-
# GET /semesters/1.json
-
1
def show
-
end
-
-
# GET /semesters/new
-
1
def new
-
1
@semester = Semester.new
-
end
-
-
# GET /semesters/1/edit
-
1
def edit
-
end
-
-
# POST /semesters
-
# POST /semesters.json
-
1
def create
-
2
@semester = Semester.new(semester_params)
-
-
2
respond_to do |format|
-
2
if @semester.save
-
-
2
format.html { redirect_to semesters_path, notice: 'Semester was successfully created.' }
-
1
format.json { render :show, status: :created, location: @semester }
-
else
-
2
format.html { render :new }
-
1
format.json { render json: @semester.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# PATCH/PUT /semesters/1
-
# PATCH/PUT /semesters/1.json
-
1
def update
-
-
2
respond_to do |format|
-
2
if @semester.update(semester_params)
-
#status_check(@semester)
-
4
format.html { redirect_to semesters_path, notice: 'Semester was successfully updated.' }
-
2
format.json { render :show, status: :ok, location: @semester }
-
else
-
format.html { render :edit }
-
format.json { render json: @semester.errors, status: :unprocessable_entity }
-
end
-
end
-
end
-
-
# DELETE /semesters/1
-
# DELETE /semesters/1.json
-
1
def destroy
-
1
@semester.destroy
-
1
respond_to do |format|
-
2
format.html { redirect_to semesters_url, notice: 'Semester was successfully destroyed.' }
-
1
format.json { head :no_content }
-
end
-
end
-
-
1
private
-
# Use callbacks to share common setup or constraints between actions.
-
1
def set_semester
-
5
@semester = Semester.find(params[:id])
-
end
-
-
# Never trust parameters from the scary internet, only allow the white list through.
-
1
def semester_params
-
#params.fetch(:semester, {})
-
4
params.require(:semester).permit(:sem_name, :status, :maximum_credit)
-
end
-
end
-
2
module ApplicationHelper
-
end
-
2
module CoursesHelper
-
end
-
2
module EnrollsHelper
-
end
-
2
module RegistersHelper
-
end
-
2
module SemestersHelper
-
end
-
2
class Course < ActiveRecord::Base
-
2
validates_presence_of :course_no, :course_title, :credit, :semester, :message => "Can't be empty"
-
2
validates_uniqueness_of :course_no, :message => "Already Exists"
-
2
validates :credit, numericality: {greater_than_or_equal_to: 0.75}
-
-
2
has_many :registers
-
2
has_many :enrolls, through: :registers
-
-
2
def course_title_with_credit
-
1
[course_title, credit].compact.join(' ')
-
end
-
-
-
#
-
# before_destroy :ensure_not_referneced_by_any_line_item
-
#
-
# private
-
#
-
# def ensure_not_referneced_by_any_line_item
-
# if line_items.empty?
-
# return true
-
# else
-
# errors.add(:base, 'Line Items present')
-
# return false
-
# end
-
# end
-
end
-
2
class Enroll < ActiveRecord::Base
-
2
before_create :semester_check
-
2
before_save :credit_check
-
-
2
belongs_to :user
-
2
belongs_to :semester
-
-
2
has_many :registers
-
2
has_many :courses, through: :registers
-
-
-
2
accepts_nested_attributes_for :registers
-
2
accepts_nested_attributes_for :courses
-
-
2
protected
-
-
2
def semester_check
-
begin
-
current_semester = Semester.find_by_status("Current").id
-
semester = Enroll.where(user_id: self.user_id, semester_id: current_semester)
-
if semester.present?
-
self.errors.add(:base, 'Already Registered!!')
-
return false
-
-
else
-
return true
-
-
end
-
end
-
end
-
-
2
def credit_check
-
total = 0
-
course_ids = self.course_ids
-
courses = Course.where(id: course_ids)
-
courses.each do |course|
-
credit = course.credit
-
total += credit
-
end
-
semester_id = self.semester_id
-
@maximum_credit = Semester.find_by(semester_id).maximum_credit
-
#@maximum_credit = 2.00
-
if total > @maximum_credit
-
self.errors.add(:base, 'Maximum Credit Exceeded!!')
-
return false
-
else
-
return true
-
end
-
end
-
end
-
-
2
class Register < ActiveRecord::Base
-
2
belongs_to :course
-
2
belongs_to :enroll
-
-
2
accepts_nested_attributes_for :course
-
#accepts_nested_attributes_for :enroll
-
-
-
-
-
end
-
2
class Semester < ActiveRecord::Base
-
2
before_save :status_check
-
2
has_many :enrolls
-
2
has_many :users, through: :enrolls
-
-
2
validates_presence_of :sem_name, :status, :message => "Can't be empty"
-
2
validates_uniqueness_of :sem_name, :message => "Already exists"
-
-
2
STATUS = ['Done', 'Current', 'Due']
-
-
2
scope :current, ->{ where(status: 'current')}
-
-
2
protected
-
-
2
def status_check
-
3
if self.status_changed? && self.status == "Current"
-
@prev = Semester.find_by_status("Current")
-
if @prev != nil
-
@prev.update_attribute(:status, "Done")
-
end
-
end
-
end
-
end
-
2
class User < ActiveRecord::Base
-
# Include default devise modules. Others available are:
-
# :confirmable, :lockable, :timeoutable and :omniauthable
-
2
devise :database_authenticatable, :registerable,
-
:recoverable, :rememberable, :trackable, :validatable
-
2
has_many :enrolls
-
2
has_many :semesters, through: :enrolls
-
-
2
accepts_nested_attributes_for :enrolls
-
-
end
-
# Load the Rails application.
-
2
require File.expand_path('../application', __FILE__)
-
-
# Initialize the Rails application.
-
2
Rails.application.initialize!
-
2
Rails.application.configure do
-
# Settings specified here will take precedence over those in config/application.rb.
-
-
# The test environment is used exclusively to run your application's
-
# test suite. You never need to work with it otherwise. Remember that
-
# your test database is "scratch space" for the test suite and is wiped
-
# and recreated between test runs. Don't rely on the data there!
-
2
config.cache_classes = true
-
-
# Do not eager load code on boot. This avoids loading your whole application
-
# just for the purpose of running a single test. If you are using a tool that
-
# preloads Rails for running tests, you may have to set it to true.
-
2
config.eager_load = false
-
-
# Configure static file server for tests with Cache-Control for performance.
-
2
config.serve_static_files = true
-
2
config.static_cache_control = 'public, max-age=3600'
-
-
# Show full error reports and disable caching.
-
2
config.consider_all_requests_local = true
-
2
config.action_controller.perform_caching = false
-
-
# Raise exceptions instead of rendering exception templates.
-
2
config.action_dispatch.show_exceptions = false
-
-
# Disable request forgery protection in test environment.
-
2
config.action_controller.allow_forgery_protection = false
-
-
# Tell Action Mailer not to deliver emails to the real world.
-
# The :test delivery method accumulates sent emails in the
-
# ActionMailer::Base.deliveries array.
-
2
config.action_mailer.delivery_method = :test
-
-
# Randomize the order test cases are executed.
-
2
config.active_support.test_order = :random
-
-
# Print deprecation notices to the stderr.
-
2
config.active_support.deprecation = :stderr
-
-
# Raises error for missing translations
-
# config.action_view.raise_on_missing_translations = true
-
end
-
# Be sure to restart your server when you modify this file.
-
-
# Version of your assets, change this if you want to expire all your assets.
-
2
Rails.application.config.assets.version = '1.0'
-
-
# Add additional assets to the asset load path
-
# Rails.application.config.assets.paths << Emoji.images_path
-
-
# Precompile additional assets.
-
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
-
# Rails.application.config.assets.precompile += %w( search.js )
-
# Be sure to restart your server when you modify this file.
-
-
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
-
-
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
-
# Rails.backtrace_cleaner.remove_silencers!
-
# Be sure to restart your server when you modify this file.
-
-
2
Rails.application.config.action_dispatch.cookies_serializer = :json
-
# Use this hook to configure devise mailer, warden hooks and so forth.
-
# Many of these configuration options can be set straight in your model.
-
2
Devise.setup do |config|
-
# The secret key used by Devise. Devise uses this key to generate
-
# random tokens. Changing this key will render invalid all existing
-
# confirmation, reset password and unlock tokens in the database.
-
# Devise will use the `secret_key_base` as its `secret_key`
-
# by default. You can change it below and use your own secret key.
-
# config.secret_key = 'fb5f10fb3daa5f76c5c4f459e662abe9e8af8d0ca70b3ac6c6bc2924824cecd21e75bfbd551497e3dd360e258e6cfe32dba220828df4a7b042cd2e7decb96c55'
-
-
# ==> Mailer Configuration
-
# Configure the e-mail address which will be shown in Devise::Mailer,
-
# note that it will be overwritten if you use your own mailer class
-
# with default "from" parameter.
-
2
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
-
-
# Configure the class responsible to send e-mails.
-
# config.mailer = 'Devise::Mailer'
-
-
# Configure the parent class responsible to send e-mails.
-
# config.parent_mailer = 'ActionMailer::Base'
-
-
# ==> ORM configuration
-
# Load and configure the ORM. Supports :active_record (default) and
-
# :mongoid (bson_ext recommended) by default. Other ORMs may be
-
# available as additional gems.
-
2
require 'devise/orm/active_record'
-
-
# ==> Configuration for any authentication mechanism
-
# Configure which keys are used when authenticating a user. The default is
-
# just :email. You can configure it to use [:username, :subdomain], so for
-
# authenticating a user, both parameters are required. Remember that those
-
# parameters are used only when authenticating and not when retrieving from
-
# session. If you need permissions, you should implement that in a before filter.
-
# You can also supply a hash where the value is a boolean determining whether
-
# or not authentication should be aborted when the value is not present.
-
# config.authentication_keys = [:email]
-
-
# Configure parameters from the request object used for authentication. Each entry
-
# given should be a request method and it will automatically be passed to the
-
# find_for_authentication method and considered in your model lookup. For instance,
-
# if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
-
# The same considerations mentioned for authentication_keys also apply to request_keys.
-
# config.request_keys = []
-
-
# Configure which authentication keys should be case-insensitive.
-
# These keys will be downcased upon creating or modifying a user and when used
-
# to authenticate or find a user. Default is :email.
-
2
config.case_insensitive_keys = [:email]
-
-
# Configure which authentication keys should have whitespace stripped.
-
# These keys will have whitespace before and after removed upon creating or
-
# modifying a user and when used to authenticate or find a user. Default is :email.
-
2
config.strip_whitespace_keys = [:email]
-
-
# Tell if authentication through request.params is enabled. True by default.
-
# It can be set to an array that will enable params authentication only for the
-
# given strategies, for example, `config.params_authenticatable = [:database]` will
-
# enable it only for database (email + password) authentication.
-
# config.params_authenticatable = true
-
-
# Tell if authentication through HTTP Auth is enabled. False by default.
-
# It can be set to an array that will enable http authentication only for the
-
# given strategies, for example, `config.http_authenticatable = [:database]` will
-
# enable it only for database authentication. The supported strategies are:
-
# :database = Support basic authentication with authentication key + password
-
# config.http_authenticatable = false
-
-
# If 401 status code should be returned for AJAX requests. True by default.
-
# config.http_authenticatable_on_xhr = true
-
-
# The realm used in Http Basic Authentication. 'Application' by default.
-
# config.http_authentication_realm = 'Application'
-
-
# It will change confirmation, password recovery and other workflows
-
# to behave the same regardless if the e-mail provided was right or wrong.
-
# Does not affect registerable.
-
# config.paranoid = true
-
-
# By default Devise will store the user in session. You can skip storage for
-
# particular strategies by setting this option.
-
# Notice that if you are skipping storage for all authentication paths, you
-
# may want to disable generating routes to Devise's sessions controller by
-
# passing skip: :sessions to `devise_for` in your config/routes.rb
-
2
config.skip_session_storage = [:http_auth]
-
-
# By default, Devise cleans up the CSRF token on authentication to
-
# avoid CSRF token fixation attacks. This means that, when using AJAX
-
# requests for sign in and sign up, you need to get a new CSRF token
-
# from the server. You can disable this option at your own risk.
-
# config.clean_up_csrf_token_on_authentication = true
-
-
# ==> Configuration for :database_authenticatable
-
# For bcrypt, this is the cost for hashing the password and defaults to 11. If
-
# using other algorithms, it sets how many times you want the password to be hashed.
-
#
-
# Limiting the stretches to just one in testing will increase the performance of
-
# your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
-
# a value less than 10 in other environments. Note that, for bcrypt (the default
-
# algorithm), the cost increases exponentially with the number of stretches (e.g.
-
# a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
-
2
config.stretches = Rails.env.test? ? 1 : 11
-
-
# Set up a pepper to generate the hashed password.
-
# config.pepper = 'cec53344287d0429aab12f66c0f1053265c542e08e3acbd81cb25f4a92f37789c2f3d2cd0d942a0ea235ef5338582b3df0cc414af177a934fce17565fb12965a'
-
-
# Send a notification email when the user's password is changed
-
# config.send_password_change_notification = false
-
-
# ==> Configuration for :confirmable
-
# A period that the user is allowed to access the website even without
-
# confirming their account. For instance, if set to 2.days, the user will be
-
# able to access the website for two days without confirming their account,
-
# access will be blocked just in the third day. Default is 0.days, meaning
-
# the user cannot access the website without confirming their account.
-
# config.allow_unconfirmed_access_for = 2.days
-
-
# A period that the user is allowed to confirm their account before their
-
# token becomes invalid. For example, if set to 3.days, the user can confirm
-
# their account within 3 days after the mail was sent, but on the fourth day
-
# their account can't be confirmed with the token any more.
-
# Default is nil, meaning there is no restriction on how long a user can take
-
# before confirming their account.
-
# config.confirm_within = 3.days
-
-
# If true, requires any email changes to be confirmed (exactly the same way as
-
# initial account confirmation) to be applied. Requires additional unconfirmed_email
-
# db field (see migrations). Until confirmed, new email is stored in
-
# unconfirmed_email column, and copied to email column on successful confirmation.
-
2
config.reconfirmable = true
-
-
# Defines which key will be used when confirming an account
-
# config.confirmation_keys = [:email]
-
-
# ==> Configuration for :rememberable
-
# The time the user will be remembered without asking for credentials again.
-
# config.remember_for = 2.weeks
-
-
# Invalidates all the remember me tokens when the user signs out.
-
2
config.expire_all_remember_me_on_sign_out = true
-
-
# If true, extends the user's remember period when remembered via cookie.
-
# config.extend_remember_period = false
-
-
# Options to be passed to the created cookie. For instance, you can set
-
# secure: true in order to force SSL only cookies.
-
# config.rememberable_options = {}
-
-
# ==> Configuration for :validatable
-
# Range for password length.
-
2
config.password_length = 6..128
-
-
# Email regex used to validate email formats. It simply asserts that
-
# one (and only one) @ exists in the given string. This is mainly
-
# to give user feedback and not to assert the e-mail validity.
-
2
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
-
-
# ==> Configuration for :timeoutable
-
# The time you want to timeout the user session without activity. After this
-
# time the user will be asked for credentials again. Default is 30 minutes.
-
# config.timeout_in = 30.minutes
-
-
# ==> Configuration for :lockable
-
# Defines which strategy will be used to lock an account.
-
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
-
# :none = No lock strategy. You should handle locking by yourself.
-
# config.lock_strategy = :failed_attempts
-
-
# Defines which key will be used when locking and unlocking an account
-
# config.unlock_keys = [:email]
-
-
# Defines which strategy will be used to unlock an account.
-
# :email = Sends an unlock link to the user email
-
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
-
# :both = Enables both strategies
-
# :none = No unlock strategy. You should handle unlocking by yourself.
-
# config.unlock_strategy = :both
-
-
# Number of authentication tries before locking an account if lock_strategy
-
# is failed attempts.
-
# config.maximum_attempts = 20
-
-
# Time interval to unlock the account if :time is enabled as unlock_strategy.
-
# config.unlock_in = 1.hour
-
-
# Warn on the last attempt before the account is locked.
-
# config.last_attempt_warning = true
-
-
# ==> Configuration for :recoverable
-
#
-
# Defines which key will be used when recovering the password for an account
-
# config.reset_password_keys = [:email]
-
-
# Time interval you can reset your password with a reset password key.
-
# Don't put a too small interval or your users won't have the time to
-
# change their passwords.
-
2
config.reset_password_within = 6.hours
-
-
# When set to false, does not sign a user in automatically after their password is
-
# reset. Defaults to true, so a user is signed in automatically after a reset.
-
# config.sign_in_after_reset_password = true
-
-
# ==> Configuration for :encryptable
-
# Allow you to use another hashing or encryption algorithm besides bcrypt (default).
-
# You can use :sha1, :sha512 or algorithms from others authentication tools as
-
# :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
-
# for default behavior) and :restful_authentication_sha1 (then you should set
-
# stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
-
#
-
# Require the `devise-encryptable` gem when using anything other than bcrypt
-
# config.encryptor = :sha512
-
-
# ==> Scopes configuration
-
# Turn scoped views on. Before rendering "sessions/new", it will first check for
-
# "users/sessions/new". It's turned off by default because it's slower if you
-
# are using only default views.
-
# config.scoped_views = false
-
-
# Configure the default scope given to Warden. By default it's the first
-
# devise role declared in your routes (usually :user).
-
# config.default_scope = :user
-
-
# Set this configuration to false if you want /users/sign_out to sign out
-
# only the current scope. By default, Devise signs out all scopes.
-
# config.sign_out_all_scopes = true
-
-
# ==> Navigation configuration
-
# Lists the formats that should be treated as navigational. Formats like
-
# :html, should redirect to the sign in page when the user does not have
-
# access, but formats like :xml or :json, should return 401.
-
#
-
# If you have any extra navigational formats, like :iphone or :mobile, you
-
# should add them to the navigational formats lists.
-
#
-
# The "*/*" below is required to match Internet Explorer requests.
-
# config.navigational_formats = ['*/*', :html]
-
-
# The default HTTP method used to sign out a resource. Default is :delete.
-
2
config.sign_out_via = :delete
-
-
# ==> OmniAuth
-
# Add a new OmniAuth provider. Check the wiki for more information on setting
-
# up on your models and hooks.
-
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
-
-
# ==> Warden configuration
-
# If you want to use other strategies, that are not supported by Devise, or
-
# change the failure app, you can configure them inside the config.warden block.
-
#
-
# config.warden do |manager|
-
# manager.intercept_401 = false
-
# manager.default_strategies(scope: :user).unshift :some_external_strategy
-
# end
-
-
# ==> Mountable engine configurations
-
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
-
# is mountable, there are some extra configurations to be taken into account.
-
# The following options are available, assuming the engine is mounted as:
-
#
-
# mount MyEngine, at: '/my_engine'
-
#
-
# The router that invoked `devise_for`, in the example above, would be:
-
# config.router_name = :my_engine
-
#
-
# When using OmniAuth, Devise cannot automatically set OmniAuth path,
-
# so you need to do it manually. For the users scope, it would be:
-
# config.omniauth_path_prefix = '/my_engine/users/auth'
-
end
-
# Be sure to restart your server when you modify this file.
-
-
# Configure sensitive parameters which will be filtered from the log file.
-
2
Rails.application.config.filter_parameters += [:password]
-
# Be sure to restart your server when you modify this file.
-
-
# Add new inflection rules using the following format. Inflections
-
# are locale specific, and you may define rules for as many different
-
# locales as you wish. All of these examples are active by default:
-
# ActiveSupport::Inflector.inflections(:en) do |inflect|
-
# inflect.plural /^(ox)$/i, '\1en'
-
# inflect.singular /^(ox)en/i, '\1'
-
# inflect.irregular 'person', 'people'
-
# inflect.uncountable %w( fish sheep )
-
# end
-
-
# These inflection rules are supported but not enabled by default:
-
# ActiveSupport::Inflector.inflections(:en) do |inflect|
-
# inflect.acronym 'RESTful'
-
# end
-
# Be sure to restart your server when you modify this file.
-
-
# Add new mime types for use in respond_to blocks:
-
# Mime::Type.register "text/richtext", :rtf
-
# Be sure to restart your server when you modify this file.
-
-
2
Rails.application.config.session_store :cookie_store, key: '_Final_session'
-
# Be sure to restart your server when you modify this file.
-
-
# This file contains settings for ActionController::ParamsWrapper which
-
# is enabled by default.
-
-
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
-
2
ActiveSupport.on_load(:action_controller) do
-
2
wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
-
end
-
-
# To enable root element in JSON for ActiveRecord objects.
-
# ActiveSupport.on_load(:active_record) do
-
# self.include_root_in_json = true
-
# end
-
2
Rails.application.routes.draw do
-
2
resources :registers
-
2
resources :enrolls
-
2
resources :semesters
-
2
devise_for :users, path_names: {sign_in: "login", sign_out: "logout"}
-
-
# devise_scope :user do
-
# root to: "devise/registrations#new"
-
# end
-
-
2
root 'enrolls#new'
-
-
2
resources :courses
-
-
2
get '/semester' => 'semesters#index'
-
-
2
get '/enrolls/edit' => 'enrolls#edit'
-
-
2
get 'semesters/show' => 'semesters@show'
-
# The priority is based upon order of creation: first created -> highest priority.
-
# See how all your routes lay out with "rake routes".
-
-
# You can have the root of your site routed with "root"
-
# root 'welcome#index'
-
-
# Example of regular route:
-
# get 'products/:id' => 'catalog#view'
-
-
# Example of named route that can be invoked with purchase_url(id: product.id)
-
# get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
-
-
# Example resource route (maps HTTP verbs to controller actions automatically):
-
# resources :products
-
-
# Example resource route with options:
-
# resources :products do
-
# member do
-
# get 'short'
-
# post 'toggle'
-
# end
-
#
-
# collection do
-
# get 'sold'
-
# end
-
# end
-
-
# Example resource route with sub-resources:
-
# resources :products do
-
# resources :comments, :sales
-
# resource :seller
-
# end
-
-
# Example resource route with more complex sub-resources:
-
# resources :products do
-
# resources :comments
-
# resources :sales do
-
# get 'recent', on: :collection
-
# end
-
# end
-
-
# Example resource route with concerns:
-
# concern :toggleable do
-
# post 'toggle'
-
# end
-
# resources :posts, concerns: :toggleable
-
# resources :photos, concerns: :toggleable
-
-
# Example resource route within a namespace:
-
# namespace :admin do
-
# # Directs /admin/products/* to Admin::ProductsController
-
# # (app/controllers/admin/products_controller.rb)
-
# resources :products
-
# end
-
end
-
2
require 'test_helper'
-
-
2
class EnrollsControllerTest < ActionController::TestCase
-
#include Devise::TestHelpers
-
2
fixtures :enrolls
-
2
setup do
-
2
@enroll = enrolls(:enroll_one)
-
end
-
-
2
test "should get index" do
-
get :index
-
assert_response :success
-
#assert_not_nil assigns(:enrolls)
-
end
-
#
-
# test "should get new" do
-
# get :new
-
# assert_response :success
-
# end
-
#
-
# test "should create enroll" do
-
# assert_difference('Enroll.count') do
-
# post :create, enroll: { course_ids: @enroll.course_ids, semester_id: @enroll.semester_id, user_id: @enroll.user_id }
-
# end
-
#
-
# assert_redirected_to enroll_path(assigns(:enroll))
-
# end
-
#
-
# test "fails create enroll" do
-
# post :create, enroll: { course_ids: @enroll.course_ids, semester_id: @enroll.semester_id, user_id: @enroll.user_id }
-
# assert_no_difference('Enroll.count') do
-
# post :create, enroll: { course_ids: @enroll.course_ids, semester_id: @enroll.semester_id, user_id: @enroll.user_id }
-
# end
-
# assert_template "edit"
-
#
-
# #assert_redirected_to edit_enroll_path(assigns(:enroll))
-
# end
-
#
-
# test "should show enroll" do
-
# get :show, id: @enroll
-
# assert_response :success
-
# end
-
#
-
# test 'should get edit' do
-
# get :edit, id: @enroll
-
# assert_response :success
-
# end
-
#
-
# test "should update enroll" do
-
# patch :update, id: @enroll, enroll: { course_ids: @enroll.course_ids, semester_id: @enroll.semester_id, user_id: @enroll.user_id }
-
# assert_redirected_to enroll_path(assigns(:enroll))
-
# end
-
#
-
# test "fails update enroll" do
-
# patch :update, id: @enroll , enroll: { course_ids: @enroll.course_ids, semester_id: @enroll.semester_id, user_id: @enroll.user_id }
-
# #assert_redirected_to edit_enroll_url(assigns(:enroll))
-
# assert_template "edit"
-
# end
-
#
-
# test "should destroy enroll" do
-
# assert_difference('Enroll.count', -1) do
-
# delete :destroy, id: @enroll
-
# end
-
#
-
# assert_redirected_to enrolls_path
-
# end
-
end
-
1
require 'test_helper'
-
-
1
class RegistersControllerTest < ActionController::TestCase
-
1
include Devise::TestHelpers
-
#fixtures :registers
-
1
setup do
-
9
@register = registers(:register_one)
-
9
@user = users(:user_one)
-
9
sign_in @user
-
end
-
-
1
test "should get index" do
-
1
get :index
-
1
assert_response :success
-
1
assert_not_nil assigns(:registers)
-
end
-
-
1
test "should get new" do
-
1
get :new
-
1
assert_response :success
-
end
-
-
1
test "should create register" do
-
1
assert_difference('Register.count') do
-
1
post :create, register: { course_id: @register.course_id, enroll_id: @register.enroll_id }
-
end
-
-
1
assert_redirected_to register_path(assigns(:register))
-
end
-
-
1
test "fails create register" do
-
1
post :create, register: { course_id: @register.course_id, enroll_id: @register.enroll_id }
-
1
assert_difference('Register.count') do
-
1
post :create, register: { course_id: @register.course_id, enroll_id: @register.enroll_id }
-
end
-
-
1
assert_redirected_to register_path(assigns(:register))
-
end
-
-
1
test "should show register" do
-
1
get :show, id: @register
-
1
assert_response :success
-
end
-
-
1
test "should get edit" do
-
1
get :edit, id: @register
-
1
assert_response :success
-
end
-
-
1
test "should update register" do
-
1
patch :update, id: @register, register: { course_id: @register.course_id, enroll_id: @register.enroll_id }
-
1
assert_redirected_to register_path(assigns(:register))
-
end
-
-
1
test "fails update register" do
-
1
post :create, register: { course_id: @register.course_id, enroll_id: @register.enroll_id }
-
1
patch :update, id: @register, register: { course_id: @register.course_id, enroll_id: @register.enroll_id }
-
1
assert_template "edit"
-
#assert_redirected_to edit_register_path
-
end
-
-
1
test "should destroy register" do
-
1
assert_difference('Register.count', -1) do
-
1
delete :destroy, id: @register
-
end
-
-
1
assert_redirected_to registers_path
-
end
-
end
-
1
require 'test_helper'
-
-
1
class SemestersControllerTest < ActionController::TestCase
-
1
include Devise::TestHelpers
-
#fixtures :semesters
-
1
setup do
-
9
@user = users(:user_one)
-
9
sign_in @user
-
-
9
@semester = semesters(:semester_one)
-
9
@update = {
-
sem_name: 'Fall16',
-
status: 'Done',
-
maximum_credit: 5.00
-
}
-
-
-
end
-
-
1
test "should get index" do
-
1
get :index
-
1
assert_response :success
-
1
assert_not_nil assigns(:semesters)
-
end
-
-
1
test "should get new" do
-
1
get :new
-
1
assert_response :success
-
end
-
-
1
test "should create semester" do
-
1
assert_difference('Semester.count') do
-
1
post :create, semester: {sem_name: 'Fall16', status: 'Due', maximum_credit: 5.00}
-
end
-
-
1
assert_redirected_to semesters_path
-
end
-
-
1
test "fails create semester" do
-
1
assert_no_difference('Semester.count') do
-
1
post :create, semester: {sem_name: semesters(:semester_one).sem_name, status: 'Due', maximum_credit: 5.00}
-
end
-
1
assert_template "new"
-
-
#assert_redirected_to new_semester_path
-
end
-
-
1
test "should show semester" do
-
1
get :show, id: @semester
-
1
assert_response :success
-
end
-
-
1
test "should get edit" do
-
1
get :edit, id: @semester
-
1
assert_response :success
-
end
-
-
1
test "should update semester" do
-
1
patch :update, id: @semester, semester: @update
-
1
assert_redirected_to semesters_path
-
end
-
-
1
test "fails update semester" do
-
1
patch :update, id: @semester, semester: {sem_name: 'Fall16', status: 'Due', maximum_credit: '5.00'}
-
1
assert_template "edit"
-
assert_redirected_to edit_semester_path
-
end
-
-
1
test "should destroy semester" do
-
1
assert_difference('Semester.count', -1) do
-
1
delete :destroy, id: @semester
-
end
-
-
1
assert_redirected_to semesters_path
-
end
-
end
-
1
require 'test_helper'
-
-
1
class CourseTest < ActiveSupport::TestCase
-
1
fixtures :courses
-
# test "the truth" do
-
# assert true
-
# end
-
-
1
test "course attributes must not be empty" do
-
1
course = Course.new
-
1
assert course.invalid?
-
# assert course.errors[:course_no].any?
-
# assert course.errors[:course_title].any?
-
# assert course.errors[:credit].any?
-
# assert course.errors[:semester].any?
-
end
-
-
1
test "course credit must be positive" do
-
1
course = Course.new(course_no: "CSE 3107",
-
course_title: "Database",
-
semester: "Fall'16")
-
1
course.credit = -1
-
1
assert course.invalid?
-
-
1
course.credit = 0
-
1
assert course.invalid?
-
end
-
-
1
test "course is not valid without a unique course no" do
-
1
course = Course.new(course_title: "xyz",
-
course_no: courses(:course_one).course_no,
-
semester: "Fall'16",
-
credit: 3.00)
-
-
1
assert course.invalid?
-
#assert_equal ["has already been taken"], course.errors[:course_title]
-
end
-
-
1
test 'course_title_with_credit' do
-
1
course = courses(:course_one)
-
1
assert_equal(course.send(:course_title_with_credit), 'Theory of Computation 2.0')
-
-
end
-
-
-
-
-
end
-
1
require 'test_helper'
-
-
1
class EnrollTest < ActiveSupport::TestCase
-
# test "the truth" do
-
# assert true
-
# end
-
1
test 'semester already exists' do
-
1
enroll = enrolls(:enroll_one)
-
assert_equal(enroll.send(:semester_check), 'Already Registered!!')
-
end
-
-
1
test 'credit must not exceed' do
-
1
enroll = enrolls(:enroll_one)
-
assert_equal(enroll.send(:semester_check), 'Maximum Credit Exceeded!!')
-
end
-
end
-
1
require 'test_helper'
-
-
1
class RegisterTest < ActiveSupport::TestCase
-
# test "the truth" do
-
# assert true
-
# end
-
end
-
1
require 'test_helper'
-
-
1
class SemesterTest < ActiveSupport::TestCase
-
1
fixtures :semesters
-
1
setup do
-
4
@new = {
-
sem_name: 'Summer17',
-
status: 'Current',
-
maximum_credit: '5.00'
-
}
-
end
-
# test "the truth" do
-
# assert true
-
# end
-
-
1
test "semester attributes must not be empty" do
-
1
semester = Semester.new
-
1
assert semester.invalid?
-
# assert semester.errors[:sem_name].any?
-
# assert semester.errors[:status].any?
-
# assert semester.errors[:maximum_credit].any?
-
end
-
-
1
test "maximum credit must be positive" do
-
1
semester = Semester.new(sem_name: "Fall'17",
-
status: "Due")
-
# semester.maximum_credit = -1
-
# assert semester.invalid?
-
#
-
# semester.maximum_credit = 0
-
# assert semester.invalid?
-
-
1
semester.maximum_credit = 5.00
-
1
assert semester.valid?
-
end
-
-
1
test "semester is not valid without a unique title" do
-
1
semester = Semester.new(sem_name: semesters(:semester_two).sem_name,
-
status: "Done",
-
maximum_credit: 5.00)
-
-
1
assert semester.invalid?
-
end
-
-
1
test 'only one semester is current' do
-
1
semester = Semester.where(status: 'Current').count
-
1
assert_equal(1, semester)
-
end
-
-
-
end
-
1
require 'test_helper'
-
-
1
class UserTest < ActiveSupport::TestCase
-
# test "the truth" do
-
# assert true
-
# end
-
end